Ваша первая программа
Для знакомства с языком Турбо Паскаля попробуем составить несложную
программу, осуществляющую вывод какого-либо сообщения на экран ПК. Пусть это
будет фраза «Я программирую на Турбо Паскале». Вот возможный вариант такой
программы:
Пример 2.1
Program My_First_Program;
const
Text = 'Я программирую на Турбо Паскале';
begin
WriteLn(Text);
end.
Прежде всего проанализируем форму представления текста. В программе шесть
строк. Строки программы обычно выделяют некоторые смысловые фрагменты текста и
могут не связываться с конкретными действиями в программе: расположение текста
программы по строкам - дело вкуса программиста, а не требование синтаксиса
языка. Ту же программу можно было бы написать, например, так:
Program My_First_Program; const Text =
'Я программирую на Турбо Паскале';begin
WriteLn(Text); end.
В отличие от некоторых других языков программирования пробел в языке Турбо
Паскаль используется как разделитель отдельных конструкций языка, поэтому
программа
PROGRAMMy_First_Program;constText=
'Я программирую на Турбо Паскале';BEGINWriteLn(Text);end.
будет неверной.
В Турбо Паскале игнорируется различие в высоте букв (заглавные или строчные),
если только это не связано с текстовыми константами. Начало программы могло бы,
например, выглядеть так:
program my_first_program;
Теперь о смысле отдельных строк. Первая строка
Program My_First_Program;
начинается словом Program и содержит объявление имени программы. Слово
Program зарезервировано в Турбо Паскале, т.е. не может использоваться ни в каких
иных целях, кроме как для объявления имени программы. В Турбо Паскале имеется
множество зарезервированных слов (см. гл.З). Любое из них нельзя использовать в
качестве идентификатора (имени) какого-либо объекта программы - переменной,
константы и т.д. Замечу, что редактор среды Турбо Паскаля обычно выделяет
зарезервированные слова цветом. В связи с этим в тексте книги эти слова выделены
жирным шрифтом. Поскольку имя программы никак в дальнейшем не используется,
требование его объявления кажется излишним. В Турбо Паскале можно опускать
объявление имени оператором Program без каких-либо последствий для программы.
В рассматриваемом примере имя My_First_Program есть не что иное, как
английская фраза «Моя Первая Программа», но только написанная без пробелов -
пробел является разделителем и не может использоваться произвольно (вместо
пробелов в идентификаторах разрешается использовать символ подчеркивания).
Первая строка заканчивается особым разделителем - точкой с запятой. Этот
разделитель в языке Турбо Паскаль отмечает конец оператора или описания.
Использование особого разделителя позволяет располагать несколько операторов на
одной строке.
Вторая строка
const
содержит единственное зарезервированное слово const, означающее, что далее
будут описаны одна или несколько констант (CONSTants -
константы). Константами в
языке считаются такие объекты программы, которые не могут изменять своего
значения. В отличие от многих других языков программирования, константа в Турбо
Паскале может иметь собственное имя, что соответствует принятой в научных и
инженерных расчетах практике именования часто используемых констант. Например,
со школы мы помним о существовании константы п—3.14159265. При обработке
программы имя константы pi будет заменяться компилятором на ее значение.Описать константу в Турбо Паскале - значит указать ее имя и значение. Такое
указание содержится в третьей строке
Text = 'Я программирую на Турбо Паскале';
в которой константе с именем Text присваивается в качестве значения строка
символов «Я программирую на Турбо Паскале».
В Турбо Паскале могут использоваться константы разного типа - целые или
вещественные числа, символы, строки символов, массивы и т.д. Признаком того, что
Text является константой типа строка символов, служат два апострофа, обрамляющих
строку, причем сами апострофы этой строке не принадлежат, а лишь указывают
компилятору на то, что все заключенные в них символы следует рассматривать как
единое целое - текстовую константу. Если понадобится включить сам апостроф в
текстовую константу, достаточно его написать дважды подряд. Например, описание
Text = 'Турбо' 'Паскаль';
создаст константу со значением
Турбо'Паскаль
Все три первые строки не связаны с какими-либо конкретными действиями при
работе программы. Они сообщают компилятору некоторые сведения о самой программе
и использующихся в ней объектах. Эта часть программы называется разделом
описаний. Зарезервированное слово begin в четвертой строке сигнализирует
компилятору о начале другой части программы - раздела операторов. В нашем
примере этот раздел содержит оператор
WriteLn(Text);
который, собственно, и выводит сообщение на экран компьютера.
Завершает всю программу зарезервированное слово end с точкой. Точка оповещает
компилятор о конце текста программы. За сочетанием end. можно размещать какой
угодно текст - он не будет обрабатываться компилятором.
Перед тем как попробовать откомпилировать и исполнить нашу программу, обсудим
ее единственный исполняемый оператор
WriteLn(Text);
Любопытно, что в Паскале вообще и Турбо Паскале, в частности, нет специальных
операторов ввода-вывода. Для обмена информацией с окружающим миром в программах,
написанных на языке Турбо Паскаль, используются специальные стандартные
процедуры. Таким образом, по своей сути оператор
WriteLn(Text);
является оператором обращения к встроенной процедуре вывода данных (свое
название она получила от WRITE LiNe - записать строку).
Понятие процедуры (см. гл.8) - одно из центральных понятий Турбо Паскаля.
Процедура - это некоторая последовательность операторов, к которой можно
обратиться по имени. Всякий раз, когда мы называем в операторе имя процедуры,
инициируется последовательность запрограммированных в ней действий.
Процедура WriteLn относится к стандартным или встроенным процедурам Турбо
Паскаля. Стандартная процедура не нуждается в предварительном описании, она
доступна любой программе, в которой содержится обращение к ней. Разница между
оператором вывода и обращением к процедуре вывода состоит в том, что имя
процедуры вывода, как и любой другой процедуры Турбо Паскаля, не является
зарезервированным словом, а следовательно, пользователь может написать свою
собственную процедуру с именем WriteLn. Впрочем, эта возможность для большинства
пользователей остается лишь языковой тонкостью и очень редко используется на
практике.
Процедура WriteLn - одна из немногих процедур Турбо Паскаля, при обращении к
которым допускается использование произвольного числа параметров. Параметры
передаются процедуре в виде списка, располагающегося в круглых скобках сразу за
именем процедуры. В нашем примере процедуре передается единственный параметр
-константа Text. Как мы увидим дальше (см. гл.5), самым первым параметром при
обращении к процедуре WriteLn можно указать адрес приемника информации - устройство или дисковый файл, в который направляется вывод. Таким способом
программист может легко переадресовать вывод данных. Если, как это сделано в
нашем примере, адрес вывода не указан, вывод направляется на экран дисплея.
Анализируя всю программу в целом, мы обнаружим, что четыре использовавшихся в
ней слова (Program, const, begin и end) являются зарезервированными. Слово
WriteLn, как уже отмечалось, не относится к зарезервированным, но вряд ли может
возникнуть необходимость переопределить его, так как в этом случае программа
лишится мощного и удобного средства вывода данных. Два слова My_First_Program и
Text служат идентификаторами (именами) некоторых объектов программы. Программист
может использовать в качестве идентификаторов любые последовательности символов,
которые удовлетворяют следующим ограничениям:
идентификатор может состоять из букв латинского алфавита, цифр, знака
подчеркивания; никакие другие символы в идентификаторе недопустимы;
идентификатор не может начинаться с цифры;
идентификатор не может совпадать ни с одним из зарезервированных слов;
длина идентификатора может быть произвольной, но значащими считаются первые
63 символа.
Как и всюду в программе, в идентификаторах игнорируется разница в высоте
букв, поэтому, например, идентификаторы Text, text и TEXT с точки зрения
компилятора идентичны.
Теперь попробуйте выполнить программу. Для этого после набора ее текста
нажмите Ctrl-F9. Если Вы не ошиблись при вводе текста, то спустя несколько
секунд заметите быструю смену изображений на экране: сразу после загрузки
программы Турбо Паскаль очищает экран, предоставляя его в распоряжение
работающей программы пользователя. Такой экран называется окном программы. После
завершения прогона (работа программы часто называется ее прогоном) на экране
вновь появится окно редактора с текстом программы. Если Вы не успели разглядеть
изображение окна программы, нажмите Alt-F5. После нажатия на любую клавишу среда
вернет экран в режим воспроизведения окна редактора.
Перед тем, как двигаться дальше, полезно подробнее ознакомиться с некоторыми
возможностями среды Турбо Паскаля. Нажмите клавишу
F10, чтобы перейти к режиму
выбора из главного меню, подведите указатель к опции
Debug (отладка) и нажмите
клавишу Enter - на экране раскроется меню второго уровня, связанное с этой
опцией. Новое меню как бы «выпало» из верхней строки, поэтому такое меню часто
называют выпадающим. Отыщите в новом меню опцию
Output (вывод программы),
подведите к ней указатель и нажмите клавишу
Enter еще раз. На экране вновь
появится окно программы, но оно уже не будет исчезать после нажатия на любую
клавишу - экран будет связан с этим окном постоянно. Теперь добьемся того, чтобы
на экране демонстрировались два окна одновременно: вновь нажмите клавишу
F10,
выберите Window, нажмите клавишу Enter, подведите указатель к опции
Tilе(черепица) и нажмите клавишу Enter еще раз. Если все сделано правильно,
экран приобретет вид, показанный на рис.2.1.
Рис. 2.1. Вид экрана с окнами редактора и программы
Двойная рамка, очерчивающая окно программы, свидетельствует о том, что именно
это окно активно в данный момент. Сделаем активным окно редактора: нажмем
клавишу Alt и, не отпуская ее, - клавишу с цифрой 1 (окно редактора имеет номер
1, окно программы - номер 2, см. верхние правые углы рамок на рис.2.1). Теперь
все готово к дальнейшим экспериментам с программой.
Попробуем изменить выводимый на экран текст. Например, уберем в конце
треть-ей строки точку с запятой и отредактируем ее таким образом:
Text = 'я учусь программировать на турбо паскале'
Если Вы запустите программу вновь, нажав клавиши Ctrl-F9, компилятор сообщит:
Error 85: ";" expected. (Ошибка 85: Отсутствует ";".),
а редактор установит курсор на первый символ слова begin, показывая то место,
где при разборе текста обнаружена ошибка (разделитель «;» может отделять от
конца оператора сколько угодно пробелов; компилятор пропускает эти пробелы в
поисках разделителя до тех пор, пока не обнаружит зарезервированное слово, - вот
почему курсор стоит не в конце строки с объявлением константы, а перед словом
begin). Исправьте программу - поставьте в конце третьей строки разделитель «;» и
вновь запустите счет. На этот раз все пройдет нормально, но вывод программы
изменится, и в окне программы появится текст
я учусь программировать на турбо паскале
Этот текст строго соответствует заданному в текстовой константе набору
символов, поэтому в нем отсутствуют прописные буквы.
Типы данных
Структура рассмотренной программы имеет следующий вид:
Program MyFirstProgram;
{Раздел описаний}
begin
{Раздел операторов}
end.
Слова Program, begin и end выделяют две части программы - раздел описаний и
раздел операторов. Такая структура обязательна для любой программы, что является
следствием жесткого требования языка: любой нестандартный идентификатор,
используемый в исполняемых операторах, должен быть предварительно описан в
разделе описаний. (Стандартные идентификаторы связаны с предварительно
объявленными объектами и входят в стандартную библиотеку Турбо Паскаля. Таким,
например, является идентификатор WriteLn. Стандартные идентификаторы, если они
используются в программе, описывать не нужно).
Требование предварительного описания идентификаторов кажется чрезмерно
строгим и делающим язык менее свободным. На самом деле в нем проявляется
тенденция развития языков программирования в сторону повышения надежности
создаваемых программ. Кто программировал на Фортране или Бэйсике (в этих языках
не требуется предварительное описание идентификаторов), знает, как порой бывает
трудно обнаружить в большой программе ошибочно введенный или пропущенный символ
в идентификаторе. Если, например, всюду в программе используется переменная с
именем EPSILON, а в одном месте ошибочно написано
EPSLION, то программа может
благополучно откомпилироваться и даже давать почти правдоподобный результат для
некоторых наборов данных, но в какой-то момент начнет вести себя странно.
Обязательное предварительное описание идентификаторов в Турбо Паскале защищает
программы от такого рода ошибок и повышает их надежность.
Описать идентификатор - это значит указать тип связанного с ним объекта
программы (константы или переменной). Понятие типа - одно из фундаментальных
понятий Турбо Паскаля. В гл.4 подробно рассмотрены различные типы; чтобы
пояснить описываемые ниже особенности языка и при этом не слишком забегать
вперед, укажем, что тип определяет, во-первых, способ внутреннего для компьютера
представления объекта и, во-вторых, действия, которые разрешается над ним
выполнять.
В рассматриваемых далее в этой главе программах понадобятся следующие типы
данных:
INTEGER - целочисленные данные, во внутреннем представлении занимают 2 байта;
диапазон возможных значений - от -32768 до +32767; данные представляются точно;
REAL - вещественные данные, занимают 6 байт; диапазон возможных значений
модуля - от 2.9Е-39 до 1.7Е+38; точность представления данных - 11...12 значащих
цифр;
CHAR - символ, занимает 1 байт;
STRING - строка символов, занимает МАХ+1 байт, где МАХ - максимальное число
символов в строке;
BOOLEAN - логический тип, занимает 1 байт и имеет два значения: FALSE
(ложь) и TRUE (истина).
Тип константы определяется способом записи ее
значения. Например:
const
cl = 17;
с2 = 3 .14 ;
сЗ = 'А';
с4 = '3.14 ' ;
с5 = False;
При анализе этого фрагмента программы компилятор отнесет первую константу к
типу INTEGER, вторую - к типу REAL, третью - к CHAR, четвертую - к STRING и
последнюю - к BOOLEAN. Признаком, позволяющим отнести константу к REAL или к
INTEGER, является наличие или отсутствие десятичной точки в ее значении.
Разумеется, константы С2 и С4 относятся к разным типам: С2 - к REAL (в константе
есть десятичная точка), а С4 - к STRING (константа обрамлена апострофами).
Константу СЗ компилятор будет считать относящейся к типу CHAR: одиночный символ
в апострофах относится к CHAR, в то время как несколько символов - к STRING.
В отличие от константы переменная именует объект программы, который может
изменять свое значение в ходе счета. При описании переменных за идентификатором
ставятся двоеточие и имя типа. Несколько однотипных переменных можно объединять
в список, разделяя их запятыми. В начале раздела описания переменных должно
стоять зарезервированное слово VAR (VARiables - переменные). Например:
var
sigma :Real; а,b,с,d :Char;
textl :String[15];
text2 :String;
flag :Boolean;.
Как уже говорилось, тип данных определяет длину внутреннего представления
соответствующих переменных. В частности, длина внутреннего представления
переменных типа STRING (строка символов) зависит от максимального числа
символов, которые могут составлять строку. В приведенном выше примере переменная
text l описана с указанием ее максимальной длины (15 символов), а в описании
переменной text2 максимальная длина не указана и компилятор установит для нее
предельно допустимую в Турбо Паскале длину - 255 символов.
Рассмотрим еще одну несложную программу (пример 2.2). Ее назначение: ввести с
клавиатуры два целых числа, найти результат деления первого числа на второе и
вывести полученный результат на экран.
Пример 2.2
Program Input_0utput; {Программа вводит два целых числа
и выводит частное от деления 1-го на 2-е}
var
n1,n2 : Integer; {n1 и n2 - вводимые целые}
х : Real;{x - результат}
BEGIN
Write( 'n1 = ');{Сообщаем о вводе
n1}
ReadLn (n1) ;{Вводим n1}
Write ( 'n2 = ');{Сообщаем о вводе п2}
ReadLn (n2);{Вводим п2}
x := n1/n2;{Находим результат}
WriteLn('n1/n2 =',х);{Выводим его}
END.
Прежде всего бросается в глаза появление в программе поясняющих комментариев.
Комментарий в Турбо Паскале - это произвольная последовательность любых
символов, обрамленная фигурными скобками. Комментарий разрешается вставлять в
любое место программы, где по смыслу может стоять пробел. В качестве
ограничителей комментария допускается использование фигурных скобок «{» и «}», а
также пары символов: «(*» - слева от комментария и «*)» - справа от него:
{ Это - комментарий }
(* Это - тоже комментарий
*)
Редактор Турбо Паскаля выделяет комментарии наклонным шрифтам (курсивом).
Комментарии с однотипными ограничителями нельзя вкладывать друг в друга, т.е.
недопустимы последовательности вида
{...{...}...} или
(*...(*...*)...*)
Однако можно вкладывать комментарии с ограничителями разных типов (не более
одной глубины вложения):
{ ... (* ...*)...} или
(* ... { ... } ... *)
Последнее обстоятельство проясняет кажущуюся странной избыточность
ограничителей: если всюду в программе будут использоваться ограничители одного
типа, то для того, чтобы временно исключить из программы какой-либо фрагмент
текста, достаточно заключить его в ограничители другого типа.
Наличие комментариев в программе избавляет меня от необходимости пояснять
назначение отдельных строк программы. Несколько слов о вводе данных. Пары
операторов
Write (..);
ReadLn(..);
работают следующим образом. Вначале оператор Write выводит строку на экран и
оставляет курсор в конце только что выведенной строки текста. Заметим, что
оператор
WriteLn(Text);
в примере 1 после вывода текста осуществлял перевод строки и устанавливал
курсор в начало следующей строки экрана. Именно в этом простом действии
(переводе строки) заключается единственное отличие в работе процедуры WriteLn от
процедуры
Write.
Затем по оператору ReadLn вызывается встроенная процедура ввода данных и
программа останавливается в ожидании ввода. В этот момент необходимо набрать на
клавиатуре нужное число и нажать клавишу Enter. Сразу после этого программа
продолжит работу: проанализирует введенное число и перейдет к вводу следующего
числа или вычислению результата. Таким образом, сигналом окончания подготовки
очередного числа является нажатие на клавишу
Enter, до этого момента можно
стирать любой ошибочно введенный символ клавишей
Backspace.
Для вычисления отношения введенных чисел используется один из основных
операторов Турбо Паскаля - оператор присваивания. В его левой части указывается
имя переменной, правая часть представляет собой выражение того же типа, что и
переменная. Пара символов «: =», связывающая левую и правую части оператора
присваивания, означает «присвоить значение». Запомним: в операторах присваивания
Турбо Паскаля всегда используются символы «: =», в то время как при описании
констант -одиночный символ «=». С точки зрения синтаксиса языка, два символа «:
=» рассматриваются как один специальный символ и обязательно пишутся слитно.
Оператор присваивания используется практически во всех языках
программирования. В некоторых языках, например в Фортране или Бейсике, символом
присваивания является знак равенства, однако новичка, привыкшего к строгости
математических формул, может озадачить типичная форма записи фортран-оператора
присваивания, например, такая:
X = X + 1
Вариант записи этого же оператора на Турбо Паскале:,
X := X + 1;
в этом смысле кажется более логичным. Разумеется, вряд ли кому-нибудь придет
в голову видеть уравнения там, где их нет и не может быть. Конечно же, и в том,
и в другом случае реализуется одно и то же алгоритмическое действие: к
содержимому X прибавляется 1 и полученный результат вновь присваивается
переменной X. Обратите внимание на оператор вывода результатов
WriteLn('n1/n2 = ',х);
В нем в качестве одного из параметров явно указывается константа типа строка
символов 'nl/n2 = '. Конечно же, константы (в отличие от переменных) вовсе не
обязательно описывать в разделе описаний, так как их тип легко определяется
компилятором по форме записи константы. С учетом этого можно было бы записать
программу из примера 1 предельно лаконично:
begin WriteLn('Я программирую на Турбо Паскале');
end.
Преобразованя типов и действия над ними
Как уже говорилось, тип переменной позволяет не только устанавливать длину ее
внутреннего представления, но и контролировать те действия, которые выполняются
над ней в программе. Контроль за использованием переменных еще на этапе
компиляции программы - важное преимущество Турбо Паскаля перед другими языками
программирования, в которых допускается автоматическое преобразование типов. В
Турбо Паскале почти невозможны неявные (автоматические) преобразования типов.
Исключение сделано только в отношении констант и переменных типа INTEGER
(целые), которые разрешается использовать в выражениях типа REAL (вещественные).
Если, например, переменные X и Y описаны следующим образом:
var
х: Integer;
у: Real;
то оператор
у := х + 2;
будет синтаксически правильным: хотя справа от знака присваивания стоит
целочисленное выражение, а слева - вещественная переменная, компилятор сделает
необходимые преобразования автоматически. В то же время оператор
х := 2.0;
будет неверным, так как автоматическое преобразование типа REAL (константа
2.0 содержит десятичную точку и, следовательно, принадлежит к типу REAL) в тип
INTEGER в Турбо Паскале запрещено.
Разумеется, запрет на автоматическое преобразование типов еще не означает,
что в Турбо Паскале нет средств преобразования данных. Они, конечно же, есть, но
их нужно использовать явно (подробнее об этом см. гл.4). Для преобразования
данных в языке существуют встроенные функции, которые получают в качестве
параметра значение одного типа, а возвращают результат в виде значения другого
типа. В частности, для преобразования REAL в INTEGER имеются даже две встроенные
функции такого рода: ROUND округляет REAL до ближайшего целого, a TRUNC усекает
REAL путем отбрасывания дробной части.
Например, ошибочным будет оператор
х := у/х;
но правильным
х := round(у/х);
(объявления переменных см. выше).
Понятие функции в Турбо Паскале близко к понятию процедуры. Как и
процедура, функция вызывается своим именем и может содержать произвольное число
операторов Турбо Паскаля и даже внутренних процедур и функций. Существенным
отличием функции от процедуры является то обстоятельство, что функция имеет
собственное значение и, следовательно, может использоваться наравне с
переменными в выражениях соответствующего типа.
Для преобразования данных типа CHAR (символ) в целое число предназначена
функция ORD, обратное преобразование INTEGER в CHAR осуществляет функция CHR.
С помощью следующей несложной программы (пример 2.3) Вы сможете узнать
внутренний код произвольного символа.
Пример 2.3
Program Code_pf_Char;
{Программа читает символ с клавиатуры и выводит на экран
этот символ несоответствующий ему внутренний код}
var
ch: Char; {В эту переменную читается символ}
begin
Write('Введите любой символ: ');
ReadLn(ch); {Читаем один символ}
WriteLn(ch,
' = ',ord(ch)); {Преобразуем его к
целому и выводим на экран}
END.
Обратите внимание: при вызове
WriteLntch,' = ',ord(ch));
третьим параметром обращения указан вызов функции ORD (СН) , что с точки
зрения языка является выражением; как мы увидим дальше (см. гл.8), во многих
случаях при вызове процедур и функций в качестве параметров вызова можно
указывать не только переменные или константы, но и выражения с их участием.
По мере надобности мы будем знакомиться с другими функциями преобразования
типов данных, а сейчас - о тех операциях, которые разрешены над различными
типами.
Конечно же, в Турбо Паскале есть все четыре арифметические операции над
переменными REAL И INTEGER:
+ - сложение;
- - вычитание;
* - умножение;
/ - деление вещественное;
div -
деление целочисленное.
Наличие двух операций деления есть еще одно проявление основополагающего
принципа Турбо Паскаля: программист должен явно подтверждать компилятору, что он
готов к возможным последствиям преобразования типов. Если, например, в языке
Фортран используется выражение 1/2 , то результат этого выражения будет зависеть
от того, переменной какого типа он будет присвоен: если N есть переменная целого
типа, а Х- вещественного, то в программе на Фортране присваивания
N = 1/2
X = 1/2
дадут значения 0 для N и 0.5 для X. В Турбо Паскале такой двусмысленности
нет: выражение 1/2 всегда имеет значение 0.5 и поэтому оператор
var
N :Integer;
begin
N := 1/2;
просто недопустим. В то же время допустимый в Турбо Паскале оператор
var
X : Real;
begin
X := 1 div 2;
самим фактом использования операции целочисленного деления DIV
свидетельствует о том, что программист сознательно отбрасывает дробную часть
результата. (Надеюсь, что читатель извинит явную искусственность этих примеров,
которая вызвана лишь стремлением проиллюстрировать обсуждаемые особенности
языка).
Для данных типа INTEGER в Турбо Паскале есть еще одна операция MOD -
получение остатка от целочисленного деления. Например:
5 mod 2 = 1
31 mod 16 = 15
18 mod 3 = 0
В Турбо Паскале отсутствует операция возведения в степень, что, очевидно,
будет вызывать определенные неудобства при реализации вычислительных алгоритмов.
Некоторым утешением может служить наличие встроенной функции SQR, возвращающей
квадрат от значения параметра, причем тип результата определяется типом
параметра.
И еще об одном существенном недостатке Турбо Паскаля: в нем отсутствуют
комплексный тип и соответствующие операции над ним. Вообще, в отношении
реализации разнообразных вычислительных процедур Турбо Паскаль значительно
уступает некоторым другим языкам программирования, в частности, тому же
Фортрану. В частности, в нем намного беднее набор встроенных математических
функций (см. гл. 4).
При работе с целыми числами могут оказаться полезными две процедуры (здесь и
далее в квадратных скобках указываются необязательные параметры):
DEC (X [, N] ) - уменьшает содержимое переменной X на значение выражения N
(если N не задано, то на 1); тип переменной X и выражения N - INTEGER (точнее,
любой целый, см. гл. 4);
INC (X [, N] ) - увеличивает значение X на N (если N не задано, то на 1).
Над символами и строками символов определена единственная операция -
сцепление двух строк. Операция обозначается символом «+». Например, программа
var
st: String;
begin
st := 'Турбо'+'-'+'Паскаль';
WriteLn(st);
end.
напечатает строку
Турбо-Паскаль
Все остальные действия над строками и символами реализуются с помощью
встроенных процедур и функций (см. гл.4).
И, наконец, об операциях отношения и логических операциях.
Над данными типа REAL, INTEGER, CHAR, STRING определены следующие операции
отношения (сравнения):
= - равно;
<> - не равно;
< - меньше;
> - больше;
<= - меньше или равно,
>= - больше или равно.
В операциях сравнения должны участвовать однотипные операнды. Исключение
сделано опять-таки в отношении REAL и INTEGER, которые могут сравниваться друг с
другом. Результат применения операции отношения к любым операндам имеет тип
BOOLEAN.
Сравнение двух строк осуществляется следующим образом. Символы строк
сравниваются попарно друг с другом так, что первый символ первой строки
сравнивается с первым символом второй строки, второй символ первой строки - со
вторым символом второй и т.д. Символы сравниваются путем сравнения их кодов во
внутреннем представлении (см. гл. 4). Если одна строка короче другой,
недостающие символы заменяются нулем. Отношение первой несовпадающей друг с
другом пары символов и принимается за отношение двух строк.
При сравнении данных типа BOOLEAN учитывается внутреннее соглашение Турбо
Паскаля, в соответствии с которым FALSE есть нулевой байт, a TRUE - байт с
единицей в младшем разряде. Заметим, что функция ORD преобразует к целому не
только символы, но и логические величины, поэтому
ord(false) = 0,
ord(true) = 1.
В Турбо Паскале определены следующие логические операции:
not - логическое НЕ;
or - логическое ИЛИ;
and - логическое И;
хоr - исключающее ИЛИ.
Логические операции применимы к операндам целого и логического типов. Если
операнды - целые числа, то результат логической операции есть тоже целое число
(подробнее об этом сказано в гл.4). Логические операции над логическими данными
дают результат логического типа.
При вычислении выражений любого типа приоритет вычислений определяется
расставленными скобками, а при их отсутствии - по табл. 2.1 (в порядке убывания
приоритета).
Таблица 2.1 Приоритет операций
Приоритет |
Операция |
1 |
not, @ |
2 |
*, /, div, mod, and, shl , shr |
3 |
+, - , or, xor |
4 |
=, <>, >, >=, <, <=, in |
Примечание. Операции @ (получение адреса), shl (сдвиг влево), shr (сдвиг
вправо) и in (принадлежность к множеству) описаны в гл.4.
Следует учесть, что в отличие от многих других языков программирования в
Турбо Паскале логические операции имеют более высокий приоритет, чем операции
отношения. В связи с этим, в сложных логических выражениях обычно необходимо
расставлять скобки. Если, например, b и с имеют тип INTEGER , то выражение
а = b and с < d
вызовет сообщение о синтаксической ошибке, так как сначала выполнится
операция b and с. Правильным будет выражение:
(а = b) and (с < d)
Операторы языка
С одним из наиболее часто используемых операторов языка Турбо Паскаль -
оператором присваивания мы уже познакомились. Ниже рассматриваются остальные
операторы языка.
Операторы языка
С одним из наиболее часто используемых операторов языка Турбо Паскаль -
оператором присваивания мы уже познакомились. Ниже рассматриваются остальные
операторы языка.
Составной оператолр и пустой оператор
Составной оператор - это последовательность произвольных операторов
программы, заключенная в операторные скобки - зарезервированные слова begin . .
. end. Составные операторы - важный инструмент Турбо Паскаля, дающий возможность
писать программы по современной технологии структурного программирования (без
операторов перехода GOTO).
Язык Турбо Паскаль не накладывает никаких ограничений на характер операторов,
входящих в составной оператор. Среди них могут быть и другие составные операторы
- Турбо Паскаль допускает произвольную глубину их вложенности:
begin
.......
begin
.......
begin
......
......
end;
......
end;
......
end;
Фактически, весь раздел операторов, обрамленный словами begin . . . end,
представляет собой один составной оператор. Поскольку зарезервированное слово
end является закрывающей операторной скобкой, оно одновременно указывает и конец
предыдущего оператора, поэтому ставить перед ним символ «;» необязательно, и
далее во всех примерах мы не будем этого делать. Наличие точки с запятой перед
end в предыдущих примерах означало, что между последним оператором и операторной
скобкой end располагается пустой оператор. Пустой оператор не содержит никаких действий, просто в программу добавляется лишняя точка с запятой. В
основном пустой оператор используется для передачи управления в конец составного
оператора.
Условный оператор
Условный оператор позволяет проверить некоторое условие и в зависимости от
результатов проверки выполнить то или иное действие. Таким образом, условный
оператор - это средство ветвления вычислительного процесса.
Структура условного оператора имеет следующий вид:
IF <условие> THEN <оператор1> ELSE <оператор2>,
где IF, THEN, ELSE - зарезервированные слова (если, то, иначе);
<условие> - произвольное выражение логического типа; <оператор1>,
<оператор2> - любые операторы языка Турбо Паскаль.
Условный оператор работает по следующему алгоритму. Вначале вычисляется
условное выражение <условие>. Если результат есть TRUE (истина), то
выполняется <оператор1>, а <оператор2> пропускается; если результат
есть FALSE (ложь), наоборот, <оператор1> пропускается, а выполняется
<оператор2>. Например:
var
х, у, max: Integer;
begin
.......
if x > max then
у := max else
y := x;
При выполнении этого фрагмента переменная Y получит значение переменной X,
если только это значение не превышает МАХ, в противном случае Y станет равно
МАХ.
Часть ELSE <оператор2> условного оператора может быть опущена. Тогда
при значении TRUE условного выражения выполняется <оператор1>, в противном
случае этот оператор пропускается:
var
х, у, max: Integer;
begin
.......
if x > max then
max := x;
Y := x;
В этом примере переменная Y всегда будет иметь значение переменной X,а в МАХ
запоминается максимальное значение X.
Поскольку любой из операторов <оператор1> и <оператор2> может
быть любого типа, в том числе и условным, а в то же время не каждый из
«вложенных» условных операторов может иметь часть ELSE <оператор2>, то
возникает неоднозначность трактовки условий. Эта неоднозначность в Турбо Паскале
решается следующим образом: любая встретившаяся часть ELSE соответствует ближайшей к ней
«сверху» части THEN условного оператора. Например:
var
a,b,c,d : Integer; begin
a := 1; b := 2; c= 3; d := 4;
if a > b then
if с < d then
if с < 0 then
с := 0 else
a := b; {а равно 1}
if a > b then
if с then
if с then
с := 0
else
else
else
a := b; {а равно 2}
Рассмотрим программу (пример 2.4), которая вводит произвольное десятичное
целое число в диапазоне 0...15, преобразует его к шестнадцатеричному и выводит
на экран полученный результат.
Пример 2.4
Program Hex;
{Программа вводит с клавиатуры целое число в диапазоне от 0 до 15,
преобразует его к шестнадцатеричной системе счисления и выводит результат на
экран}
var
n : Integer; {Вводимое число}
ch : Char; {Результат}
begin
Write ( ',n = ' ) ;
ReadLn(n); { Вводим число }
{Проверяем число на принадлежность к диапазону
0...15}
if (n >= 0) and (n <= 15) then
begin {Да, принадлежит диапазону}
if n < 10 then
ch := chr(ord('0') + n)
else
ch := chr(ord('A') + n- 10);
WriteLn('n = ',ch)
end
else {He принадлежит диапазону}
WriteLn('Ошибка')
end.
В шестнадцатеричной системе счисления используется 16 цифр в каждом разряде:
цифры 0...9 обозначают первые 10 возможных значений разряда, буквы A...F -
остальные шесть.
В программе учитывается непрерывность и упорядоченность множеств цифр 0...9,
букв A...F и их кодов (см. гл.4).
Операторы повторений
В языке Турбо Паскаль имеются три различных оператора, с помощью которых
можно запрограммировать повторяющиеся фрагменты программ.
Счетный оператор
цикла FOR имеет такую структуру:
FOR <пар_цик> :=
<нач_знач> ТО <кон_знач> DO <оператор>.
Здесь FOR, TO, DO - зарезервированные слова (для, до, выполнить);
<пар_цик> - параметр цикла - переменная типа INTEGER (точнее, любого
порядкового типа, см. гл.4);
<нач_знач> - начальное значение - выражение того же типа;
<кон_знач> - конечное значение - выражение того же типа;
<оператор>
- произвольный оператор Турбо Паскаля.
При выполнении оператора FOR вначале вычисляется выражение <нач_знач> и
осуществляется присваивание <пар_цик> : = <нач_знач>. После этого
циклически повторяется:
проверка условия <пар_цик> <= <кон_знач>; если условие не
выполнено, оператор FOR завершает свою работу;
выполнение оператора <оператор>;
наращивание переменной <пар_цик> на единицу.
В качестве иллюстрации применения оператора FOR рассмотрим программу,
осуществляющую ввод с клавиатуры произвольного целого числа N и вычисление суммы
всех целых чисел от 1 до N (пример 2.5).
Пример 2.5
Program Summ_of_Integer;
{Программа вводит целое положительное число N и подсчитывает сумму всех целых
чисел от 1 до N}
var
i, n, s : Integer;
begin
Write('N = ');
ReadLn(n); . {Вводим N}
s := 0; {Начальное значение суммы}
for i : = 1 to n do {Цикл подсчета суммы}
s : = s + i;
writeln('Сумма = ',s) {Выводим результат}
end.
Отметим два обстоятельства. Во-первых, условие, управляющее работой оператора
FOR, проверяется перед выполнением оператора <оператор>: если условие не
выполняется в самом начале работы оператора FOR, исполняемый оператор не будет
выполнен ни разу. Другое обстоятельство - шаг наращивания параметра цикла строго
постоянен и равен (+1). Существует другая форма оператора:
FOR<пар_цик>: = <нач_знач> DOWNTO <кон_знач> DO
<оператор>
Замена зарезервированного слова ТО на DOWNTO означает, что шаг наращивания
параметра цикла равен (-1), а управляющее условие приобретает вид
<пар_цик> = <кон_знач>.
Пример 2.5 можно модифицировать так, чтобы сделать его пригодным для подсчета
любых сумм - положительных и отрицательных:
..................
s := 0;
if n >= 0 then
for i := 1 to n do
s := s + i else
for i := -1 downto n do s : = s + i ;
...............
Два других оператора повторений лишь проверяют условие выполнения или
повторения цикла, но не связаны с изменением счетчика цикла.
Оператор цикла WHILE с предпроверкой условия:
WHILE <условие> DO <оператор>.
Здесь WHILE, DO - зарезервированные слова (пока [выполняется условие],
делать);
<условие> - выражение логического типа;
<оператор> -
произвольный оператор Турбо Паскаля.
Если выражение <условие> имеет значение TRUE, то выполняется
<оператор>, после чего вычисление выражения <условие> и его
проверка повторяются. Если <условие> имеет значение FALSE , оператор WHILE
прекращает свою работу.
Рассмотрим пример 2.6, иллюстрирующий использование оператора WHILE. Найдем
так называемое «машинное эпсилон» - такое минимальное, не равное нулю
вещественное число, которое после прибавления его к 1.0 еще дает результат,
отличный от 1.0.
Пример 2.6
Program EpsilpnDetect;
{Программа вычисляет и выводит на экран значение "машинного эпсилон"}
var
epsilon: Real;
begin
epsilon := 1;
while epsilon/2 + 1 > 1 do
epsilon := epsilon/2
WriteLn('Машинное эпсилон = ',epsilon)
end.
У читателя, привыкшего к непрерывной вещественной арифметике, может вызвать
недоумение утверждение о том, что в дискретной машинной арифметике всегда
существуют такие числа 0<X<eps, что 1.0+Х=1.0. Дело в том, что внутреннее
представление типа REAL может дать «лишь» приблизительно 1014 возможных
комбинаций значащих разрядов в отведенных для него 6 байтах. Конечно же, это
очень большое число, но оно несопоставимо с бесконечным множеством вещественных
чисел. Аппроксимация бесконечного непрерывного множества вещественных чисел
конечным (пусть даже и очень большим) множеством их внутреннего машинного
представления и приводит к появлению «машинного эпсилон».
Оператор цикла REPEAT... UNTIL с постпроверкой условия:
REPEAT <тело_цикла> UNTIL <условие>.
Здесь REPEAT, UNTIL- зарезервированные слова (повторять до тех пор, пока не
будет выполнено условие);
<тело_цикла> - произвольная последовательность операторов Турбо
Паскаля;
<условие> - выражение логического типа.
Операторы <тело_цикла> выполняются хотя бы один раз, после чего
вычисляется выражение <условие>: если его значение есть FALSE, операторы
<тело_цикла> повторяются, в противном случае оператор REPEAT. . . UNTIL
завершает свою работу.
Для иллюстрации применения оператора REPEAT... UNTIL модифицируем программу
из примера 2.3. Модификация (пример 2.7) состоит в том, что программа будет все
время повторять цикл ввода символа и печати его кода до тех пор, пока очередным
символом не будет символ CR (вводится клавишей Enter).
Пример 2.7
Program Codes_of_Chars;
{Программа вводит символ и выводит на экран его код. Для завершения работы
программы нужно дважды нажать Enter}
var
ch : Char; {Вводимый символ}
const
CR = 13; {Код символа CR}
begin
repeat
ReadLn(ch);
WriteLn(ch,' = ',ord(ch))
until ord(ch) = CR
end.
Обратите внимание: пара REPEAT... UNTIL подобна операторным скобкам begin. ..
end, поэтому перед UNTIL ставить точку с запятой необязательно.
Для гибкого управления циклическими операторами FOR, WHILE и REPEAT в состав
Турбо Паскаля включены две процедуры:
BREAK - реализует немедленный выход из цикла; действие процедуры заключается
в передаче управления оператору, стоящему сразу за концом циклического
оператора;
CONTINUE - обеспечивает досрочное завершение очередного прохода цикла;
эквивалент передачи управления в самый конец циклического оператора.
Введение в язык этих процедур практически исключает необходимость
использования операторов безусловного перехода GOTO (см. ниже п.2.4.5).
Оператор выбора
Оператор выбора позволяет выбрать одно из нескольких возможных продолжений
программы. Параметром, по которому осуществляется выбор, служит ключ выбора
-выражение любого порядкового типа (любого из рассмотренных, кроме типов REAL и
STRING, см. гл. 4).
Структура оператора выбора такова:
CASE <ключ_выбора> OF <список_выбора> [ELSE <операторы>]
END
Здесь CASE, OF, ELSE, END - зарезервированные слова (случай, из, иначе,
конец);
<ключ_выбора> - ключ выбора;
<список_выбора> - одна или
более конструкций вида:
<константа_выбора> : <оператор>;
<константа_выбора> - константа того же типа, что и выражение<ключ_выбopa> ;
<операторы> - произвольные операторы Турбо
Паскаля.
Оператор выбора работает следующим образом. Вначале вычисляется значение
выражения <ключ_выбора>, а затем в последовательности операторов
<список_выбора> отыскивается такой, которому предшествует константа,
равная вычисленному значению. Найденный оператор выполняется, после чего
оператор выбора завершает свою работу. Если в списке выбора не будет найдена
константа, соответствующая вычисленному значению ключа выбора, управление
передается операторам, стоящим за словом ELSE. Часть ELSE <оператор> можно
опускать. Тогда при отсутствии в списке выбора нужной константы ничего не
произойдет и оператор выбора просто завершит свою работу.
Составим программу (пример 2.8), имитирующую работу микрокалькулятора.
Программа вводит две строки: первая содержит два произвольных числа, разделенных
пробелом, вторая - символ арифметического действия, например:
2 2
*
или
18.35 0.12
/
Над введенными числами осуществляется соответствующее действие и результат
выводится на экран. Признаком конца работы программы служит ввод любого символа,
отличного от +,-,*, /.
Пример 2.8
Program Calc;
{Программа вводит два числа в первой строке и один из знаков +, -, *, / - во
второй и выводит на экран результат соответствующего арифметического действия}
var
operation : Char; {Знак операции}
х, у, z : Real; {Операнды и результат}
stop : Boolean; {Признак ошибочной операции
и останова}
begin
stop := false;
repeat
WriteLn; {Пустая строка-разделитель}
Write('x,y= ' ) ;
ReadLn(x,y);
Write('операция: ') ;
ReadLn(operation);
case operation of
' + ' : |
z : = |
x + y; |
' - ' : |
z : = |
x - y; |
' * ' : |
z : = |
x * y; |
' / ' : |
z : = |
x / y; |
else |
|
|
stop |
:= true; |
end; |
|
|
if not |
stop |
then |
WriteLn('результат=',z)
until stop
end.
Любому из операторов списка выбора может предшествовать не одна, а несколько
констант выбора, разделенных запятыми. Например, следующая программа при вводе
одного из символов: у или Y выведет на экран слово «Да», а при вводе
n или N -
слово «Нет»:
var
ch : Char ;
begin
ReadLn (ch) ;
case ch of
'n','N' : WriteLn ('Нет' );
'y','Y' : WriteLn ('Да')
end
end.
Метки и операторы перехода
Можно теоретически показать, что рассмотренных операторов вполне достаточно
для написания программ любой сложности. В этом отношении наличие в языке
операторов перехода кажется излишним. Более того, современная технология
структурного программирования основана на принципе «программировать без GOTO»:
считается, что злоупотребление операторами перехода затрудняет понимание
программы, делает ее запутанной и сложной в отладке.
Тем не менее, в некоторых случаях использование операторов перехода может
упростить программу.
Оператор перехода имеет вид:
GOTO <метка>.
Здесь GOTO - зарезервированное слово (перейти [на метку]); <метка> -
метка.
Метка в Турбо Паскале - это произвольный идентификатор, позволяющий именовать
некоторый оператор программы и таким образом ссылаться на него. В целях
совместимости со стандартным языком Паскаль в языке Турбо Паскаль допускается в
качестве меток использование также целых чисел без знака.
Метка располагается непосредственно перед помечаемым оператором и отделяется
от него двоеточием. Оператор можно помечать несколькими метками, которые в этом
случае отделяются друг от друга двоеточием. Перед тем как появиться в программе,
метка должна быть описана. Описание меток состоит из зарезервированного слова
LABEL (метка), за которым следует список меток:
label
loop, 1b1, 1b2;
begin
.......
goto 1b1;
.......
loop: ......
.......
1b1:1b2: ......
........
goto 1b2;
.......
Действие оператора GOTO состоит в передаче управления соответствующему
меченному оператору.
При использовании меток необходимо руководствоваться следующими правилами:
метка, на которую ссылается оператор GOTO, должна быть описана в разделе
описаний и она обязательно должна встретиться где-нибудь в теле программы;
метки, описанные в процедуре (функции), локализуются в ней, поэтому передача
управления извне процедуры (функции) на метку внутри нее невозможна.
Массивы
Рассмотренные выше простые типы данных позволяют использовать в программе
одиночные объекты - числа, символы, строки и т.п. В Турбо Паскале могут
использоваться также объекты, содержащие множество однотипных элементов. Это
массивы -формальное объединение нескольких однотипных объектов (чисел, символов,
строк и т.п.), рассматриваемое как единое целое. К необходимости применения
массивов мы приходим всякий раз, когда требуется связать и использовать целый
ряд родственных величин. Например, результаты многократных замеров температуры
воздуха в течение года удобно рассматривать как совокупность вещественных чисел,
объединенных в один сложный объект - массив измерений.
При описании массива необходимо указать общее число входящих в массив
элементов и тип этих элементов. Например:
var
а : array [1..10] of Real;
b : array [0..50] of Char;
с : array [-3..4] of Boolean;
Как видим, при описании массива используются зарезервированные слова ARRAY и
OF (массив, из). За словом ARRAY в квадратных скобках указывается тип-диапазон,
с помощью которого компилятор определяет общее число элементов массива.
Тип-диапазон (подробнее см. в гл.4) задается левой и правой границами изменения
индекса массива, так что массив А состоит из 10 элементов, массив В - из 51, а
массив С - из 8 элементов. За словом OF указывается тип элементов, образующих
массив.
Доступ к каждому элементу массива в программе осуществляется с помощью
индекса - целого числа (точнее, выражения порядкового типа, см. гл.4), служащего
своеобразным именем элемента в массиве (если левая граница типа-диапазона равна
1, индекс элемента совпадает с его порядковым номером). При упоминании в
программе любого элемента массива сразу за именем массива должен следовать
индекс элемента в квадратных скобках, например:
var
a: array [1..10] of Integer;
b: array [0..40] of Char;
c: array [-2..2] of Boolean;
k: Integer; begin
b[17] := 'F1;
c[-2] := a[l] > [2] ;
for k : = 1 to 10 do a[k] := 0;
...
end.
В правильно составленной программе индекс не должен выходить за пределы,
определенные типом-диапазоном. Например, можно использовать элементы А[1],
В[38], С[0], но нельзя A[0] или С[38] (определение массивов см. выше). Турбо
Паскаль может контролировать использование индексов в программе на этапе компиляции и
на этапе счета программы.
Для иллюстрации приемов работы с массивами составим программу (пример 2.9),
которая создает массив случайных целых чисел, подсчитывает их среднее
арифметическое, а также определяет и выводит на экран минимальное и максимальное
из этих чисел.
Пример 2.9
Program Average;
{Программа создает массив из N случайных целых чисел, равномерно
распределенных в диапазоне от 0 до MAX_VALUE-1, подсчитывает среднее
арифметическое этих чисел, а также минимальное и максимальное из них.}
const
N = 1000;
MAX_VALUE = 100+1; {Диапазон
значений случайных чисел}
var
m : array [1..N] of Integer; {Массив чисел}
i : Integer; {Индекс массива}
max, min : Integer; {Максимальное и минимальное число}
s : Real; {Сумма чисел}
begin
{Наполняем массив случайными числами:}
for i := 1 to N do
m[i] := random(MAX_VALUE); {Задаем начальные значения переменных:}
s := 0;
max : = m [ 1 ] ;
min : = m [ 1 ] ;
{Цикл вычисления суммы всех случайных чисел и поиска минимального и
максимального:}
for i := 1 to N do
begin
s : = s + m [ i ] ;
if m[i] < min then
min := m[i]
else if m[i] > max then
max := m[i]
end;
{Вычисляем среднее значение и печатаем результат:}
WriteLn('MMH = ',min,'
Макс = ', max, ' Среднее = ',s/N)
end.
Для создания массива используется встроенная функция RANDOM (MAX) , которая
возвращает случайное целое число, равномерно распределенное в диапазоне от 0 до
МАХ-1 (МАХ- параметр обращения).
Процедуры и функции
Процедуры и функции представляют собой важный инструмент Турбо Паскаля,
позволяющий писать хорошо структурированные программы. В структурированных
программах обычно легко прослеживается основной алгоритм, их нетрудно понять
любому читателю, они проще в отладке и менее чувствительны к ошибкам
программирования. Все эти свойства являются следствием важной особенности
процедур (функций), каждая из которых представляет собой во многом
самостоятельный фрагмент программы, связанный с основной программой лишь с
помощью нескольких параметров. Самостоятельность процедур (функций) позволяет
локализовать в них все детали программной реализации того или иного
алгоритмического действия и поэтому изменение этих деталей, например, в процессе
отладки обычно не приводит к изменениям основной программы.
Многие примеры в этой книге невелики по размерам (не более 30-40 строк),
поэтому написать такие программы можно и без процедур. Иное дело - создание
крупных программ в сотни, тысячи и десятки тысяч строк. Писать такие программы
как нечто единое целое, без расчленения на относительно самостоятельные
фрагменты, т.е. без структурирования, просто невозможно. Практически во всех
языках программирования имеются средства структурирования. Языки, в которых
предусмотрены такие механизмы, называются процедурно-ориентированными. К их
числу принадлежит и Турбо Паскаль.
Процедурой в Турбо Паскале называется особым образом оформленный фрагмент
программы, имеющий собственное имя. Упоминание этого имени в тексте программы
приводит к активизации процедуры и называется ее вызовом. Сразу после
активизации процедуры начинают выполняться входящие в нее операторы, после
выполнения последнего из них управление возвращается обратно в основную
программу и выполняются операторы, стоящие непосредственно за оператором вызова
процедуры (рис.2.2).
Рис.2.2. Взаимодействие вызывающей программы и процедуры
Для обмена информацией между основной программой и процедурой используется
один или несколько параметров вызова. Как мы увидим дальше (см. гл. 8),
процедуры могут иметь и другой механизм обмена данными с вызывающей программой,
так что параметры вызова могут и не использоваться. Если они есть, то они
перечисляются в круглых скобках за именем процедуры и вместе с ним образуют
оператор вызова процедуры.
Функция отличается от процедуры тем, что результат ее работы возвращается в
виде значения этой функции, и, следовательно, вызов функции может использоваться
наряду с другими операндами в выражениях.
С примерами процедур и функций мы уже сталкивались - это стандартные
процедуры чтения И записи READ, READLN, WRITE, WRITELN, функции ORD, CHR,
математические функции и др. Стандартными они называются потому, что созданы
одновременно с системой Турбо Паскаль и являются ее неотъемлемой частью. В Турбо
Паскале имеется много стандартных процедур и функций. Наличие богатой библиотеки
таких программных заготовок существенно облегчает разработку прикладных
программ. Однако в большинстве случаев некоторые специфичные для данной
прикладной программы действия не находят прямых аналогов в библиотеках Турбо
Паскаля, и тогда программисту приходится разрабатывать свои, нестандартные
процедуры и функции.
Нестандартные процедуры и функции необходимо описать, чтобы компилятор мог
установить связь между оператором вызова и теми действиями, которые
предусмотрены в процедуре (функции). Описание процедуры (функции) помещается в
разделе описаний и внешне выглядит как программа, но вместо заголовка программы
фигурирует заголовок процедуры (функции).
Не вдаваясь в дальнейшие подробности, попробуем составить собственную
процедуру, чтобы пояснить сказанное. Пусть в этой процедуре преобразуется
некоторая символьная строка таким образом, чтобы все строчные буквы заменялись
соответствующими прописными. В Турбо Паскале имеется стандартная функция UPCASE
(см. гл.4), которая выполняет аналогичные действия над одиночным символом. Наша
процедура (назовем ее UPSTRING) будет преобразовывать сразу все символы строки,
причем сделаем ее пригодной не только для латинских букв, но и для букв русского
алфавита.
Разработку программы проведем в два этапа. Сначала сконструируем основную
(вызывающую) часть программы. Ее действия очень просты: она должна ввести
входную строку (назовем ее Sinp) с клавиатуры, преобразовать ее с помощью
процедуры UpString в выходную строку Sout и напечатать результат. Эти действия
нетрудно запрограммировать, например:
Program CharsConvert;
Procedure UpString(si: String; var s2:
String);
begin {UpString}
s2 := s1 {Пока еще нет преобразования!}
end; {UpString}
var
Sinp, Sout : String; {Исходная и преобразованная строки}
begin {Начало основной (вызывающей) программы}
Write('Введите строку: ');
ReadLn(Sinp); {Вводим исходную строку}
UpString(Sinp,Sout); {Преобразуем ее.к
прописным буквам}
WriteLn (' Результат: ',Sout) {Печатаем результат}
end. {Конец вызывающей программы}
В этой программе используется замещение процедуры UPSTRING так называемой
«заглушкой», т.е. процедурой, в которой на самом деле не осуществляется нужных
нам действий, а выходная строка просто копирует входную. (Однако эта программа
синтаксически абсолютно правильна и при желании ее можно запустить на счет.)
Заглушка понадобилась нам по двум причинам. Во-первых, приведенная программа
очень проста, в ней отсутствует детальная реализация процедуры и это позволяет
наглядно проиллюстрировать механизм ее описания. Во-вторых, на ее примере мы
знакомимся с универсальным методом конструирования сложных программ, получившим
название нисходящее программирование. В соответствии с этим методом создание
программы начинается «сверху», т.е. с разработки самого главного, генерального
алгоритма. На верхнем уровне обычно еще не ясны детали реализации той или иной
части программы, поэтому эти части следует заменить временными заглушками.
Желательно, чтобы временный вариант программы был синтаксически правильным,
тогда можно его откомпилировать и убедиться в отсутствии в нем синтаксических
ошибок. Такой прогон даст определенную уверенность перед разработкой и
реализацией алгоритмов нижнего уровня, т.е. перед заменой заглушек реально
работающими процедурами. Если реализуемый в заглушке алгоритм достаточно сложен,
его вновь структурируют, выделяя главный алгоритм и применяя новые заглушки, и
т.д. Процесс продолжается «вниз» до тех пор, пока не будет создан полностью
работоспособный вариант программы.
В дальнейшем мы еще не раз будем использовать метод нисходящего
программирования, а сейчас вернемся к описанию нашей процедуры. Как видим, это
описание начинается зарезервированным словом Procedure, за которым следуют имя
процедуры и список формальных параметров. Список параметров заключается в
круглые скобки и содержит перечень параметров с указанием их типа. Заметим, что
перед параметром s2, с помощью которого в вызывающую программу возвращается
результат преобразования, стоит зарезервированное слово VAR. Именно таким
способом компилятору указываются те параметры, в которых процедура возвращает
вызвавшей ее программе результат своей работы (подробнее см. гл. 8).
Зарезервированное слово Procedure, имя процедуры и список ее параметров образуют
заголовок процедуры. За заголовком следует тело процедуры, содержащее новый
раздел описаний (этот раздел пока еще пуст) и раздел исполняемых операторов
(оператор s2 : = s1).
Приступим к разработке алгоритма процедуры. Для этого обратимся к таблице
кодировки символов, используемой в ПК (см. прил. 2). В соответствии с этой
таблицей коды символов латинских строчных букв от а до z образуют непрерывный
массив монотонно нарастающих чисел от 97 до 122, а коды соответствующих им
прописных букв - непрерывный массив чисел от 65 до 90. Преобразование строчных
латинских букв в прописные, следовательно, состоит в уменьшении кода буквы на
32. Сложнее обстоит дело с символами русского алфавита (кириллицей). В
зависимости от принятого способа кодировки русские строчные буквы могут
образовывать один сплошной массив (кодировки ГОСТ и MIC), два массива
(альтернативная кодировка), несплошной массив (кодировка типа ЕСТЕЛ),
неупорядоченный массив (кодировка КОИ-8). Если исключить два последних варианта
кодировки, использовавшихся на устаревших ПК, то задача преобразования буквы
состоит в том, чтобы к внутреннему коду русской буквы А (для букв от а до п) или
к коду буквы Р (для букв от р до я) прибавить разницу в кодах текущего символа и
кодах букв а и и. Например, если преобразуется
буква б, то к коду А нужно прибавить разницу между кодами а и б, т.е.
единицу, в результате получим код буквы Б. Точно так же при преобразовании буквы
ф к коду буквы П будет прибавлено число 5 (как разница кодов ф и п), поэтому в
результате получится код буквы Ф. С учетом этого можно составить следующий
алгоритм реализации процедуры: для каждого символа исходной строки s1
определить, к какому подмассиву a...z, a...p или п...я принадлежит код этого
символа, и затем изменить его, добавив к кодам букв А (латинская), А (русская)
или Я соответствующую разницу. Если символ не принадлежит ни к какому из
подмассивов, нужно поместить его код в выходную строку без изменений.
Вот возможный вариант процедуры:
Procedure UpString(sl: String; var s2: String);
var
i: Integer; {Счетчик цикла преобразования}
с: Char; {Рабочая переменная
преобразования}
begin {UpString}
s2 := ' '; {Вначале выходная строка пуста}
{Цикл посимвольного анализа
исходной строки}
for i := 1 to Length(si) do
begin
{Берем из входной строки очередной символ}
с := sl[i];
{Проверяем символ на принадлежность к одному из трех подмассивов}
if (с >=
'a') and (с <= 'z') then
с := chr(ord('A')+ord(c)-ord('a1)) {А,а - латинские!}
else if (c >= 'a')
and (c <= 'n') then
с := chr(ord('A')+ord(с)-ord('a')) {А,а - русские!}
else if (c >= 'p') and
(с <= 'я') then
с := chr(ord('PI)+ord(c)-ord('p'));
s2 := s2+c
end
end; {UpString}
В процедуре вначале с помощью оператора
s2 := '';
подготавливается «пустая» выходная строка, т.е. строка нулевой длины. Затем
используется цикл от 1 до длины входной строки s1 (эта длина получается с
помощью стандартной функции Length), в ходе которого проверяется принадлежность
очередного символа указанным подмассивам и осуществляется необходимая коррекция
его внутреннего кода. Для доступа к отдельным символам строки используется
замечательное свойство типа данных STRING, позволяющее рассматривать строку как
набор (массив) символов. Первый символ этого набора имеет индекс 1, второй - 2 и
т.д. Индекс указывается сразу за именем строки в квадратных скобках. Таким
образом, s1 [i] -это i-ый символ строки s1. Преобразованный символ добавляется в
конец выходной строки.
Добавив комментарии и поместив тело процедуры вместо заглушки в
первоначальный вариант программы, получим окончательно ее рабочий вариант
(пример 2.10).
Пример 2.10
Program CharsConvert;
{Программа вводит произвольную текстовую строку, преобразует все входящие в
нее буквы в прописные и печатает результат преобразования}
PROCEDURE UpString(sl : String; var s2 :
String);
{Эта процедура преобразует буквы входной строки si в прописные буквы
латинского или русского алфавита и помещает результат преобразования в выходную
строку s2. Используется предположение о том, что последовательности латинских
букв от «а» до «z» и русских букв. от «а» до «п» и от «р» до «я», а также
последовательности соответствующих им прописных букв образуют непрерывные
массивы}
var
i: Integer; {Счетчик цикла преобразования}
с: Char; {Рабочая переменная
преобразования}
begin {UpString}
s2 :=' ' ; {Вначале выходная строка пуста}
{Цикл посимвольного анализа
исходной строки}
for i := 1 to Length(s1) do
begin
{Берем из входной строки очередной символ}
с := s1[i] ;
{Проверяем символ на принадлежность к одному из трех подмассивов}
if (с >=
'a') and (с <= 'z') then
с := chr(ord('А')+ord(c)-ord('a')) {A,a - латинские!}
else if (c >= 'a')
and (c <= 'n') then
с := chr(ord('A')+ord(c)-ord('a')) {A,a -русские!}
else if (c >= 'p') and
(с <= 'я') then
с := chr(ord('P')+ord(c)-ord('p'));
s2 := s2+c
end
end; {UpString}
var
Sinp, Sout : String; {Исходная и преобразованная строки}
begin {Начало основной (вызывающей) программы}
Write('Введите строку: ');
ReadLn(Sinp); {Вводим исходную строку}
UpString(Sinp,Sout); {Преобразуем ее к
прописным буквам}
WriteLn(' Результат: ',Sout) {Печатаем результат}
end. {Конец вызывающей программы}
Рассмотрим иной способ реализации той же программы: оформим алгоритм
преобразования в виде функции. Кроме того, с помощью стандартной функции UPCASE
преобразуем каждый очередной символ (это преобразование осуществляется только
для букв латинского алфавита) и тем самым исключим проверку принадлежности
символа к строчным латинским буквам:
Function UpString(s1: String): String;
var
i : Integer; с : Char;
s2: String; {Результат преобразования}
begin {UpString}
s2 := ' ';
for i := 1 to Length(si) do
begin
{Получаем и преобразуем очередной символ}
с := UpCase(si [i]);
if (с >= 'a') and (с <= 'п') then
с := chr(ord('A')+orcKcJ-ord('a'))
else
if (c >= 'p') and (с <= 'я') then
с := chr(ord('P')+ord(c)-ord('p'));
s2 := s2+c
end;
UpString := s2 {Присваиваем значение функции UpString}
end; {UpString}
var
Sinp: String;
begin {Начало основной программы}
Write('Введите строку: ') ;
ReadLn(Sinp);
WriteLn(' Результат: ',UpString(Sinp))
end. {Конец основной программы}
Программа получилась несколько проще за счет того, что функцию можно
использовать в качестве параметра обращения к другой процедуре (в нашем случае к
WriteLn). Обратите внимание: в теле любой функции нужно осуществить присваивание
ей вычисленного значения (см. оператор UpString := s2). В левой части оператора
присваивания в этом случае указывается имя функции.
Примеры программ
Мы познакомились с основными возможностями языка Турбо Паскаль. Как видите,
ядро языка очень компактно и отличается простотой - именно в этом состоит
главная заслуга автора Паскаля Н.Вирта: язык, придуманный им, прост и
естественен, он легко осваивается, на нем не трудно писать самые разнообразные
программы. Конечно, рассмотрены далеко не все свойства Турбо Паскаля, ведь его
главная отличительная черта - это богатство типов данных. Однако уже рассмотренного вполне
достаточно для написания многих полезных программ.
Приводимые ниже программы относительно сложны, поэтому они реализуются
поэтапно, по методу нисходящего программирования. Мне кажется, что тем
читателям, кто не имеет большого опыта в программировании или кто захочет
подробнее ознакомиться с нисходящим программированием, изучение этой главы
принесет определенную пользу. Если Вам будет скучно разбираться в «кухне»
программирования, но Вас заинтересуют описываемые здесь программы и Вы захотите
их повторить, то в прил.5 Вы найдете полный текст соответствующей программы;
однако в каждой из них используются некоторые дополнительные возможности языка
Турбо Паскаль, которые не рассматривались ранее и которые обсуждаются в
пропущенных Вами фрагментах книги.
При оформлении программ я стремился использовать хороший стиль написания
программ, т.е. такую их форму, которая дает наиболее полное представление о
структуре программы в целом и ее отдельных частей. Не существует какого-либо
стандарта, определяющего хороший стиль программы. Обычно это интуитивное понятие
включает способ расположения операторов и описаний по строкам (не рекомендуется
размещать более одного оператора на каждой строке), а также выделение отступами
тела составных и условных операторов. Последнее особенно важно в программах
Турбо Паскаля: сплошь и рядом в них встречаются операторные скобки begin... end,
причем часто вложенные друг в друга; использование отступа служит дополнительным
средством проверки правильности их расстановки - не случайно в редакторе среды
предусмотрена соответствующая опция. Принятый мною стиль оформления программ не
претендует на эталон, просто мне кажется, что таким образом оформленные
программы читаются лучше. Если Вы всерьез намерены программировать на Турбо
Паскале, имеет смысл составить собственное представление о хорошем стиле и далее
неукоснительно придерживаться его - очень скоро некоторые дополнительные
издержки на подготовку программ с лихвой окупятся их «читабельностью», а это
поможет Вам вспомнить все детали реализации программы, которая была написана
несколько месяцев тому назад.
Вычисление дня недели
Случалось ли Вам мучительно вспоминать, какой именно день недели приходился
на то или иное число год или два назад, или вычислять, на какой день недели в
этом году приходится Ваш день рождения? Если да, то Вас, думаю, заинтересует
простая программа, позволяющая по заданной дате мгновенно вычислить
соответствующий день недели. В ее основе лежит такая формула:
день недели = остаток от деления X на 7,
где X = abs(trunc(2.6*m-0.2)+d+y/4+y+c/4-2*c);
m - номер месяца (см. ниже);
d
- число (день месяца);
с - номер столетия (см. ниже);
у - номер года в столетии.
При использовании этой формулы следует учесть два обстоятельства. Во-первых,
формула верна для григорианского календаря нового стиля (от 1582 до 4903 года).
Во-вторых, год и месяц следует предварительно преобразовать так, как если бы
начало года приходилось на 1 марта. Иными словами, март в этой формуле имеет
порядковый номер 1, апрель 2, ..., январь 11 и февраль 12, причем январь и
февраль следует отнести к предыдущему году. Например, для 1 февраля 1991 года
номер месяца должен быть равен 12, а год 1990, в то время как для 31 декабря
1991 года номер месяца - 10, а год - 1991. Результат вычисления дается в виде
целого числа в диапазоне от 0 до 6, причем 0 соответствует воскресенью.
Приступим к разработке программы. Прежде всего, предположим, что программа
уже создана и Вы осуществляете ее прогон. Какая форма взаимодействия с
программой кажется Вам наиболее подходящей? Вряд ли Вас удовлетворит однократное
ее исполнение (ввод некоторой даты и вывод на экран соответствующего дня
недели). Скорее всего Вы захотите повторить работу программы для нескольких дат,
например, поинтересоваться, в какой день недели Вы родились, затем, на какой
день недели приходится в этом году Ваш день рождения, дни рождения близких,
друзей; может быть, определить, в какой день родились известные Вам исторические
деятели, и т.д. Таким образом, в программе следует предусмотреть многократное
выполнение действий <ввод даты> - <вычисление дня недели>, причем
число циклов вычисления заранее не известно. Сразу же возникает новый вопрос:
как сообщить программе, что Вы завершаете работу с ней? Для этого можно
условиться, что ввод некоторой заранее обусловленной или недопустимой даты
должен интерпретироваться программой, как указание на прекращение работы. С
учетом сказанного, напишем такой начальный вариант программы:
var
IsCorrectDate: Boolean; {Признак правильной даты}
d,m,y : Integer; {Вводимая
дата - день, месяц и год}
begin
repeat
{Ввести в переменные d, л? и у очередную дату и проверить ее. Если дата
правильная, установить IsCorrectDate=True, иначе
IsCorrectDate=False}
if
IsCorrectDate then
{Вычислить и выдать на экран день недели};
until not IsCorrectDate
end.
Если Вы попытаетесь запустить эту программу на счет, то ее поведение будет
зависеть от начального значения переменной IsCorrectDate. Это значение случайно,
так как компилятор Турбо Паскаля не проводит начальной инициализации переменных.
Скорее всего, тот байт оперативной памяти, в котором она разместится, окажется
нулевым, что в Турбо Паскале расценивается как логическое значение FALSE,
поэтому с большой вероятностью ничего не произойдет, и программа сразу же
завершит свою работу (условие not IsCorrectDate будет выполнено). Если начальное
значение IsCorrectDate окажется не нулевым, то цикл REPEAT. . .UNTIL будет
выполняться до тех пор, пока Вы не выключите компьютер или не нажмете клавиши
Ctrl-Break.
Будем считать, что необходимые действия осуществляются в двух процедурах с
именами InputDate (ввод даты) и WriteDay (вычисление и печать дня недели). В
процедуру InputDate не нужно ничего передавать из программы, так как в ней самой
осуществляются ввод и контроль даты. Поэтому заголовок процедуры может иметь
такой вид:
Procedure InputDate(var d,m,y: Integer; var
correctly:
Boolean);
Процедура WriteDay, напротив, только получает из программы нужные ей данные и
ничего не возвращает в программу, поэтому в ее заголовке параметры описываются
без слова VAR:
Procedure WriteDay(d,m,у : Integer);
С учетом этого программу можно уточнить
следующим образом:
var
IsCorrectDate: Boolean; {Признак правильной даты}
d,m,y : Integer; {Вводимая дата - день, месяц и год}
{...............................}
Procedure InputDate(var d,m,y : Integer;
var correctly : Boolean);
{Вводит в переменные d, m и у очередную дату и проверяет ее. Если дата
правильная, устанавливает correctly=true, иначе correctly=false }
begin
{InputDate}
correctly := false
end; {InputDate}
{...............................}
Procedure WriteDay(d,m,у: Integer);
{Вычисляет день недели и выводит его на
экран}
begin {WriteDay}
end; {WriteDay}
{..............................}
begin
repeat
InputDate(d,m,y,IsCorrectDate);
if IsCorrectDate then
WriteDay(d,m,y)
until not IsCorrectDate
end.
Теперь можно разработать процедуру INPUTDATE. Ввод даты не вызывает
трудностей - стандартные процедуры WRITE и READLN отлично приспособлены для этой
цели. Для проверки правильности даты нужно проверить принадлежность месяца
диапазону 1...12 и года - диапазону 1582...4903. Кроме того, число не должно
выходить из диапазона 1...31. Если Вы не очень настаиваете на более точной
проверке числа в зависимости от месяца и года (для февраля), то программная
реализация процедуры будет следующей:
Procedure InputDate(var d,m,y : Integer;
var correctly : Boolean);
{Вводит в переменные d, m и у очередную дату и проверяет ее. Если дата
правильная, устанавливает correctly=true, иначе correctly=false }
begin
{InputDate}
Write('Введите дату в формате ДД ММ ГГ: ');
ReadLn(d,m,y);
correctly := (d>=l)and (d<=31) and (m>=l)
and (m<=12) and (y>=1582) and (y<=4903)
end; {InputDate}
При выполнении этой процедуры ввод, например, трех нулей приведет к
присвоению переменной CORRECTLY значения FALSE, что вызовет завершение работы
программы.
Теперь разберемся с процедурой WRITEDAY. Получив в параметрах обращения день,
месяц и год, она должна:
преобразовать месяц и год так, как описано выше (год должен начинаться 1
марта);
вычислить день недели;
выдать на экран результат.
Первое и второе действия очень просты и легко программируются. Что касается
выдачи на экран, то можно потребовать от программы, чтобы эта выдача была не
просто числом от 0 до 6, а одной из строк «воскресенье», «понедельник», ...,
«суббота». Для этого потребуются дополнительные усилия: нужно сначала создать
массив строковых констант с именем, например, DAYS_OF_WEEK (дни_недели), а затем
выбрать из этого массива и выдать на экран нужную строку. Создать массив
текстовых констант можно с помощью объявления типизированной константы (см. гл.
7):
const
Days_of_week: array [0..6] of String [11] =
('воскресенье','понедельник','вторник',
'среда','четверг','пятница','суббота');
В этом объявлении идентификатор Days_of_week описывается в разделе констант,
однако справа от него указан тип данных (массив строк), как если бы описывалась
переменная, а уже только после типа стоит знак равенства и заключенный в круглые
скобки список элементов массива. В результате получим следующую процедуру:
Procedure WriteDay(d,m,y : Integer);
const
Days_of_week: array [0..6] of String [11] =
('воскресенье','понедельник','вторник', ' среда', ' четверг', ' пятница', '
суббота.') ;
var
с, w :Integer;
begin
if m <3 then
begin {Месяц январь или
февраль}
m := m + 10;
у := у - 1
end
else
m : = m - 2; {Остальные месяцы}
с := у div 100; {Вычисляем столетие}
у := у mod 100; {Находим год в столетии}
w := abs(trunc(2.6*m-0.2)+d+y div 4+y+c div 4-2*c) mod 7;
WriteLn(Days_of_week[w] )
end;
Окончательный вариант программы приведен в прил.5.1.
Биоритмы
Давно известно, что творческая и физическая активность человека не остается
постоянной, циклически меняется, причем периодичность ее изменения
приблизительно согласуется с периодом вращения Луны вокруг Земли. Существует
теория, согласно которой физическая, эмоциональная и интеллектуальная активность
человека подчиняется соответствующим биоритмам. Каждый биоритм представляет
собой синусоиду со строго постоянным периодом, причем для каждого биоритма
существует свой период. В отдельные дни все три биоритма человека могут
достигнуть своего максимума и тогда человек испытывает подъем творческих и
физических сил, в такие дни у него все спорится, он легко решает проблемы,
которые в другое время ему решить гораздо сложнее. Точно также существуют и
«черные» дни, соответствующие спаду всех трех биоритмов.
Используя уже опробованную методику нисходящего программирования, создадим
программу, в которой запрашивается дата рождения человека и дата, для которой
требуется оценить его состояние. Программа должна рассчитать и выдать на экран
ближайшие к этой дате дни пика и спада биоритмов.
Алгоритм программы можно укрупнено записать следующим образом:
ввести дату рождения и текущую дату, проконтролировать их правильность и
непротиворечивость;
вычислить количество дней между двумя датами, чтобы определить фазу
синусоид для текущей даты;
вычислить количество дней от текущей даты до даты ближайшего пика биоритмов
и даты ближайшего спада;
определить и напечатать обе даты.
Будем считать, что каждое из перечисленных действий реализуется в отдельной
процедуре, тогда начальный вариант программы будет таким:
Procedure InputDates(var dO,mO,yO,d,m,y:
Integer);
{Вводит дату рождения и текущую дату. Контролирует правильность дат и их
непротиворечивость (текущая дата должна быть позже даты рождения) }
begin {InputDates}
end; {InputDates}
{..........................}
Procedure Get_count_pf_days (dO,mO,yO,d,m,y:
Integer;
var days: Integer);
{Определяет полное количество дней, прошедших от одной даты до другой}
begin {Get_count_of_days}
end; {Get_count_of_days}
{--------------------------}
Procedure FindMaxMin (var dmin,dmax:
Integer; days: Integer);
{Ищет
критические дни}
begin {FindMaxMin}
end; {FindMaxMin}
{--------------------------}
Procedure WriteDates ( dmin , dmax , days :
Integer);
{Определяет критические даты по количеству дней, прошедших от
момента рождения, и выдает эти даты на экран}
begin {WriteDates}
end; {WriteDates}
{--------------------------}
var
d0,d , {Дни рождения и текущий}
m0,m, {Месяцы рождения и текущий}
у0,у, {Годы рождения и текущий}
dmin, {Наименее благоприятный день}
dmax, {Наиболее благоприятный день}
days: Integer; {Количество дней от рождения}
begin {Главная программа}
Input-Dates (d0,m0,y0,d,m,y) ;
Get_numbers_of_days (d0,m0,y0,d,m,y,days) ;
FindMaxMin (dmin, dmax, days) ;
WriteDates (dmin, dmax, days)
end .
Начинаем детализацию программы. Прежде всего подумаем, как по двум датам
вычислить разделяющее их количество дней? Если вспомнить, что следует учитывать
неодинаковое количество дней по месяцам года, а также 29 февраля для високосных
лет, то ответ на этот вопрос окажется не таким уж простым. Предлагаемый алгоритм
подсчета количества дней заключается в вычислении количества дней от даты
рождения до конца месяца, а затем и года рождения, количества дней, от начала
текущего года до текущего месяца и текущей даты, а также - в подсчете количества
полных лет, разделяющих обе даты. Количество лет затем легко пересчитывается в
количество дней с учетом длины года (365 дней для обычных и 366 дней для
високосных лет). Это очень прямолинейный алгоритм, но, откровенно говоря, мне не
пришло в голову ничего другого. Возможно, существует более изящный способ
подсчета и Вы его знаете, тогда программная реализация будет другой.
Упростить алгоритм можно за счет создания и использования массива из 12 целых
чисел, содержащего количества дней по месяцам невисокосного года, т.е. 31, 28,
31, 30 и т.д. Этот массив (назовем его SIZE_OF_MONTH - длина _месяца) можно
использовать и для обратной задачи, т.е. для определения даты критических дней,
а также для проверки правильности вводимых дат. Таким образом, массив SIZE__OF_MONTH
будет использоваться сразу в трех процедурах. Сделаем его глобальным, для чего
его описание поместим перед описанием процедур:
const
Size_of_Month: array - [1. .12] of Byte =
(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
{--------------------------}
Procedure InputDates (var d0,m0,y0,d,m,y:
Integer);
.........
Поскольку описание массива размещается до описания процедур, он становится
доступным внутри каждой из процедур и служит для них глобальным. В отличие от
этого все константы и переменные, объявляемые внутри некоторой процедуры,
являются локальными и могут использоваться только в этой процедуре.
С учетом сказанного напишем следующий начальный вариант программной
реализации процедуры INPUTDATES:
Procedure InputDates(var d0,m0,y0,d,m,y:
Integer);
{Вводит дату рождения и текущую дату. Контролирует правиль-
ность дат и их непротиворечивость (текущая дата должна быть
позже даты рождения)}
var
correctly: Boolean; {Признак правильного ввода}
begin {InputDates}
repeat
{Вводим и контролируем дату рождения d0,m0,y0.}
{Вводим и контролируем
текущую дату d,m,y.}
{Проверяем непротиворечивость дат:}
correctly := у > у0;
if not correctly and (у = y0) then
begin
correctly := m > m0;
if not correctly and (m = m0) then
correctly := d>=d0
end
until correctly
end; {InputDates}
В этой процедуре дважды выполняется одно и то же алгоритмическое действие
(ввод и контроль даты). Это действие можно вынести в отдельную внутреннюю
процедуру с именем INPDATE, тогда получим следующий окончательный вариант:
Procedure InputDates(var d0,m0,y0,d,m,y : Integer);
{Вводит дату рождения и
текущую дату. Контролирует правильность дат и их непротиворечивость (текущая
дата должна быть позже даты рождения)}
var
correctly: Boolean; {Признак правильного ввода}
{--------------------------}
Procedure InpDate (text: String; var d,m,y:
Integer);
{Выводит приглашение TEXT, вводит дату в формате ДД ММ ГГГГ и
проверяет ее правильность}
const
YMIN = 1800; {Минимальный правильный год}
YМАХ = 2000; {Максимальный
правильный год}
begin {InpDate}
repeat
Write (text);
ReadLn (d,m,y) ;
correctly := (y >= YMIN) and (Y <= YMAX) and (m >= 1)
and (m <= 12) and (d > 0) ; if correctly then
if (m = 2) and (d = 29) and (y mod 4=0) then
{Ничего не делать: это 29 февраля високосного года!}
else
correctly := d <= Size_of_Month[m] ;
if not correctly then
WriteLn (' Ошибка в дате!')
until correctly
end; {InpDate}
{--------------------------}
begin {InputDates}
repeat
InpDate (' .Введите дату рождения в формате ДД ММ ГГГГ:',d0,m0,y0) ;
InpDate (' Введите текущую дату: ',d,m,y);
{Проверяем непротиворечивость
дат:}
correctly := у > у0; if not correctly and (y = y0) then
begin
correctly := m > m0;
if not correctly and (m = m0) then
correctly := d >= d0
end
until correctly
end; {InputDates}
В самом общем виде алгоритм подсчета количества дней, разделяющих две даты,
описан выше. При его реализации следует учесть три возможных варианта:
месячный младенец (год и месяц обеих дат одинаков): количество дней
находится простым вычитанием двух чисел;
годовалый младенец (год обеих дат совпадает): количество дней = (остаток
дней в месяце рождения) + (количество дней в текущем месяце) + (количество дней
в месяцах, разделяющих обе даты);
общий вариант (отличаются года): количество дней = (количество дней от даты
рождения до конца года) + (количество дней в разделяющих даты годах) +
(количество дней от начала текущего года до текущей даты).
С учетом этого составим начальный вариант программной реализации процедуры
GET_NUMBERS_OF_DAYS :
Procedure Get_numbers_of_days (d,m,y,d,m,y:
Integer;
var days: Integer);
{Определение полного количества дней, прошедших от одной даты до другой }
{--------------------------}
Procedure Variant2;
{Подсчет количества
дней в месяцах,разделяющих обе даты}
begin {Variant2}
end; {Variant2}
{--------------------------}
Procedure Variant3 ;
{Подсчет количества дней в месяцах и годах, разделяющих обе
даты}
begin {Variant3}
end; {Variant3}
{--------------------------}
begin {Get_numbers_of_days}
if (y = y0) and (m = m0) then {Даты отличаются только днями: }
days := d - d0
else {Даты отличаются не только днями:}
begin
days := d + Size_of_Month [m0] - d0;
{Учитываем количество дней в текущем
месяце и количество дней до конца месяца рождения}
if (y0 mod 4=0) and (m0 = 2)
then
inc(days); {Учитываем високосный год}
if у = y0 then
Variant2 {Разница в месяцах одного и того же года}
else
Variant3 {Даты отличаются годами}
end
end; {Get_numbers_of_days}
В этом фрагменте используется способ связи вспомогательных процедур VARIANT2
и VARIANT3 с основной процедурой через глобальные переменные, которыми являются
параметры обращения к основной процедуре. Вспомогательные процедуры удобнее
всего реализовать на основе циклов WHILE:
Procedure Variant2 ;
{Подсчет количества дней в месяцах, разделяющих обе даты }
var
mm : Integer;
begin {Variant2}
mm : = m0 ;
while mm < m do
begin
days := days + Size_of_Month [mm] ;
if (mm = 2) and (y0 mod 4=0) then
inc (days) ;
inc (mm)
end
end; {Variant2}
{--------------------------}
Procedure Variant3;
{Подсчет количества дней в месяцах и годах, разделяющих обе
даты }
var
mm/ УУ : Integer;
begin {Variant3}
mm : = m0 + 1 ;
while mm <= 12 do {Учитываем остаток года рождения:}
begin
days := days+Size_of_Month [mm] ;
if (mm = 2) and (y0 mod 4=0) then
inc (days) ;
inc (mm)
end ;
yy := y0 + 1;
while yy < у do {Прибавляем разницу лет:}
begin
days : = days + 365;
if yy mod 4=0 then
inc (days) ;
inc (yy)
end;
mm : = 1 ;
while mm < m do {Прибавляем начало текущего года:}
begin
days := days + Size_of_Month [mm] ;
if (y mod 4=0) and (mm = 2) then
inc (days) ;
inc (mm)
end
end; {Variant3}
В процедуре FINDMAXMIN осуществляется поиск критических дней, т.е. ближайших
к текущей дате дней, для которых все три биоритма достигают своего максимума и
минимума. Предполагается, что биоритмы изменяются по законам синуса от
количества прожитых дней с периодами ТF, ТE и TI соответственно для физической,
эмоциональной и интеллектуальной активности человека. В программе приняты
следующие периоды (в днях):
Знакомство с языком Турбо Паскаля
TF= 23.6884
ТЕ= 28.4261
TI= 33.1638
Самый простой алгоритм поиска заключается в том, чтобы вычислить значения
сумм всех трех синусоид для текущего дня и для каждого из последующих дней на
некотором заранее обусловленном интервале, например, в пределах месяца.
Сопоставив результаты расчетов для каждого дня, нетрудно определить критические
дни:
Procedure FindMaxMin(var dmin,dmax: Integer;
days: Integer);
{Поиск
критических дней}
const
TF = 2*3.1416/23.6884;{Период физической активности}
ТЕ = 2*3.1416/28.4261;{Период эмоциональной активности}
TI = 2*3.1416/33.1638;{Период интеллектуальной активности}
INTERVAL =30; {Интервал прогноза}
var
min, {Накапливает минимум биоритмов}
max, {Накапливает максимум биоритмов}
x : Real; {Текущее значение биоритмов}
i : Integer;
begin {FindMaxMin}
max := sin(days*TF)+sin(days*TE)+sin(days*TI);
min := max; {Начальное значение минимума и максимума равно
значению биоритмов для текущего дня}
dmin := days;
dmax := days;
for i := 0
to INTERVAL do
begin
x := sin((days+i)*TF) + sin((days+i)*TE) +
sin((days+i)*TI);
if x > max then
begin
max : = x;
dmax : = days + i
end
else if x < min then
begin
min := x;
dmin := days + i
end
end;
end; {FindMaxMin}
При разработке алгоритма процедуры WRITEDATES, с помощью которой на экран
выводится результат работы программы, учтем, что основные сложности будут
связаны с определением новой даты по начальной дате и количеству прошедших дней.
Этот насчет будет повторяться дважды - для даты пика и даты спада биоритмов,
поэтому его следует вынести в отдельную процедуру WRITEDATES. Кроме того, вряд ли Вы
откажетесь от возможности вывода на экран дополнительной информации о том,
сколько полных дней, часов, минут и секунд разделяют дату рождения человека и
текущую дату. Однако реализация этого вывода не столь проста, как это может
показаться на первый взгляд. Дело в том, что диапазон возможных значений данных
типа INTEGER составляет от -32768 до +32767. Средняя продолжительность жизни
человека - около 70 лет, т.е. 25550 дней. Это значение еще можно представить в
Переменной типа INTEGER, однако часы, минуты и тем более секунды средней
продолжительности жизни далеко превышают этот диапазон. Чтобы получить вывод
достоверных данных, необходимо расширить диапазон значений целых чисел. Для
этого в Турбо Паскале предусмотрен специальный тип данных LONGINT («длинный»
целый), имеющий диапазон значений от -2147483648 до +2147483647 (см. гл. 4).
Поэтому в процедуре WRITEDATES следует предусмотреть вспомогательную переменную
этого типа, присвоить ей значение переменной DAYS и уже затем использовать
«длинную» переменную для вычисления (и вывода) часов, минут, секунд. В
результате начальный вариант процедуры WRITEDATES может быть таким:
Procedure WriteDates (dmin,dmax,days :
Integer);
{Определение и вывод дат критических дней. Вывод дополнительной информации о
количестве прожитых дней, часов, минут и секунд }
{---------------------}
Procedure WriteDate (text : String; dd :
Integer);
{Определение даты для дня DD от момента рождения. В глобальных
переменных d, m и у имеется текущая дата, в переменной DAYS -количество дней, прошедших от момента рождения до текущей
даты.Выводится сообщение TEXT и найденная дата в формате
ДД-МЕС-ГГГГ}
begin {WriteDate}
end; {WriteDate}
{---------------------}
var
LongDays: Longlnt; {"Длинная" целая переменная для часов,минут и секунд }
begin {Wri teDates}
LongDays : = days ;
WriteLn( 'Прошло: ', LongDays,' дней, ' , longDays*24, ' часов, ',
LongDays*24*60, ' минут, ', LongDays*24*60*60, ' секунд');
WriteDate (
'Наименее благоприятный день: ', drain);
WriteDate ( 'Наиболее благоприятный
день: ',dmax)
end; {WriteDates}
Реализация процедуры WRITEDATE не вызывает особых сложностей:
Procedure WriteDate (text: String; dd: Integer);
const
Names_of_Monthes : array [1..12] of String [3] =('янв','фев','мар','апр','мая', 'июн','июл','авг','сен','окт', 'ноя','дек');
var
d0,m0,y0,ddd : Integer;
begin {WriteDate}
d0 := d;
m0 := m;
y0 : = y;
ddd :=
days;
while ddd<>dd do begin
inc(d0); {Наращиваем число}
if (y0 mod 4 <> 0) and (d0 >
Size_of_Month[m0]) or (y0 mod ,4=0) and (d0=30) then
begin {Корректируем месяц}
d0 := 1;
inc(m0);
if m0 = 13 then {Корректируем год}
begin
m0 := 1;
inc(y0)
end
end;
inc(ddd)
end;
WriteLn(text,d0,'-',Names_of_Monthes[m0] ,'-',y0)
end; {WriteDate}
Собрав воедино отдельные части, получим полный текст программы (прил.5.2),
предназначенной для определения биоритмов.
Игра ним
Ним - одна из самых старых и увлекательных математических игр. Для игры в ним
необходим партнер (в ним играют вдвоем), стол и набор фишек. В качестве фишек
обычно используются камешки или монетки. В наиболее известном варианте нима 12
фишек раскладываются в три ряда так, как показано на рис. 2.3.
Рис.2.3. Фишки, расположенные для игры в ним по схеме 3-4-5
Правила нима просты. Игроки по очереди забирают одну или несколько фишек из
любого ряда. Не разрешается за один ход брать фишки из нескольких рядов. Выигрывает
тот, кто возьмет последнюю фишку (фишки).
Если Вы сыграете несколько партий в ним, то скоро заметите, что существует
некоторая оптимальная последовательность ходов, которая гарантирует победу, если
только Вы начинаете игру и первым ходом берете две фишки из первого ряда. Любой
другой ход даст шанс Вашему сопернику, который в этом случае наверняка победит,
если, в свою очередь, воспользуется оптимальной стратегией.
Полный анализ игры с обобщением на любое число рядов с любым числом фишек в
каждом ряду впервые опубликовал в 1901 г. профессор математики из Гарвардского
университета Чарльз Л.Бутон, который и назвал игру «ним» от устаревшей формы
английских глаголов «стянуть», «украсть». Открытая им оптимальная стратегия
основана на двоичной системе счисления и довольно проста. Каждую комбинацию
фишек Бутон назвал либо опасной, либо безопасной: если позиция, создавшаяся
после очередного хода игрока, гарантирует ему победу, она называется безопасной,
если такой гарантии нет - опасной. Бутон строго доказал, что любую опасную
позицию всегда можно превратить в безопасную нужным ходом. Наоборот, если перед
очередным ходом игрока уже сложилась безопасная позиция, то любой его ход
превращает позицию в опасную. Таким образом, оптимальная стратегия состоит в
том, чтобы каждым ходом опасную позицию превращать в безопасную и заставлять
противника «портить» ее. Использование оптимальной стратегии гарантирует победу
игроку только тогда, когда он открывает партию и начальная позиция фишек опасна
или он делает второй ход, а начальная позиция безопасна.
Чтобы определить, опасна позиция или безопасна, нужно количество фишек в
каждом ряду записать в двоичной системе счисления. Если сумма чисел в каждом
столбце (разряде) равна нулю или четна, позиция безопасна. Если же сумма нечетна
хотя бы в одном разряде, то позиция опасна. Например, для начальной позиции по
схеме 3-4-5 получим:
Десятичная запись количества фишек |
Двоичная запись количества фишек |
3 |
011 |
4 |
100 |
5 |
101 |
Сумма по разрядам 212
Сумма цифр в среднем столбце равна 1 - нечетному числу, что свидетельствует
об опасности этой позиции. Поэтому первый игрок может сделать ее безопасной для
себя, если он возьмет две фишки из первого ряда. В результате в первом ряду
остается только 1 фишка (двоичное число также 1), и сумма чисел в среднем
столбце изменится на ноль.
В привычной нам десятичной системе счисления емкость каждого разряда равна
10, а для записи значений разряда используются цифры от 0 до 9. В двоичной
системе счисления емкость каждого разряда равна 2, а из всех цифр используются
только 0 и 1. В этой системе число записывается в виде суммы степеней двойки и
при переходе от одного разряда к соседнему левому вес разряда увеличивается в 2
раза. Если нужно записать число 2 в двоичной системе, следует действовать точно
так же, как при записи числа 10 в десятичной системе: записать ноль в первом
(младшем) разряде и единицу - слева от него, т.е. 10 в двоичной системе означает
2 в десятичной системе. Точно так же 100 в двоичной системе означает 4 в
десятичной, 1000 - 8 и т.д.
Для перевода любого целого положительного числа из десятичной системы в
двоичную можно использовать прием последовательного деления числа на 2.
Например, для перевода десятичного числа 11 в двоичную систему используется
такая цепочка делении:
Делимое |
Результат |
Остаток |
11 |
5 |
1 |
1 |
2 |
1 |
2 |
1 |
0 |
Если, начиная с последнего результата, остатки от деления записать в обратном
порядке, получим 1011 - это и есть двоичное представление десятичного числа 11.
В этом легко убедиться, записав двоичное число 1011 как сумму степеней 2:
1х23+1х22+1х21+1 = 11
Попробуем разработать программу, которая будет выполнять роль партнера в
игре, причем это будет весьма опасный противник, так как он будет «знать»
оптимальную стратегию и умело ею пользоваться.
Представим себе на минутку, что Вы уже создали программу и начинаете работу с
ней. Как организовать удобное взаимодействие с программой? Конечно, возможно
простейшее решение: Вы предварительно раскладываете на столе монетки, по запросу
программы вводите в нее Ваш ход, затем читаете на экране ход программы, делаете
нужные изменения в раскладке монет и т.д. Вряд ли Вас удовлетворит такая
программа. Гораздо эффектнее имитировать на экране игровое поле с фишками и
своеобразное табло игры, в котором сообщается об очередных ходах противников.
Однако использованные ранее средства вывода данных (процедуры WRITE и WRITELN)
недостаточны для этих целей, ведь с их помощью нельзя указать конкретное место
на экране, куда нужно поместить выводимую информацию. Вывод этими процедурами
всегда начинается с той позиции на экране, которую в данный момент занимает
курсор. Следовательно, для вывода текста в нужное место экрана требуется перед
обращением к этим процедурам изменить положение курсора. Для этих целей служит
процедура GOTOXY, которая хотя и является стандартной, но располагается в
отдельной библиотеке (модуле) с именем CRT. Подробнее о модулях мы поговорим в
гл.9, а сейчас просто учтем, что процедуры и функции из дополнительных библиотек
становятся доступны в программе, если в самом ее начале объявить об их
использовании. Так, указание об использовании библиотеки CRT делается таким
образом:
Uses CRT;
После такого указания программе становятся доступны дополнительные процедуры
и функции, с помощью которых можно организовать гибкое управление текстовым
экраном, в том числе процедура GOTOXY, перемещающая курсор в произвольное место
на экране.
Теперь попробуем составить алгоритм главной программы. В простейшем виде он
таков:
Uses CRT; {Подключение библиотеки дополнительных
процедур и функций для управления экраном}
var
exit: Boolean; {Признак окончания работы}
begin
{Подготовить экран к работе}
repeat
{Ввести, проконтролировать и отобразить ход игрока}
{Найти и отобразить ход программы}
until exit
end.
В этом алгоритме выделяются три главных действия и организуется цикл, который
будет выполняться до тех пор, пока где-то в программе переменной EXIT (выход) не
будет присвоено значение TRUE.
Вначале экран подготавливается к работе: формируется игровое поле с фишками и
выводится информация о правилах игры. Как уже говорилось, ним позволяет играть с
произвольным количеством фишек. Разумно ввести в программу возможность, которая
бы позволила пользователю самому указывать число рядов и количество фишек в
рядах, т.е. настраивать программу на нужную раскладку фишек. Можно несколько
модифицировать главную программу, чтобы предусмотреть эту возможность:
Uses CRT; {Подключение библиотеки дополнительных
процедур и функций для управления экраном}
var
exit : Boolean; {Признак окончания работы}
change : Boolean; {Признак
изменения условий игры}
{----------------------}
Procedure Prepare; {Готовит экран к игре}
begin {Prepare}
end; {Prepare}
{----------------------}
Procedure GetPlayerMove;
{Получает, контролирует и отображает ход игрока}
begin {GetPlayerMove}
end; {GetPlayerMove}
{----------------------}
Procedure SetOwnerMove;
{Находит и отображает очередной ход программы}
begin {SetOwnerMove}
end; {SetOwnerMove}
{----------------------}
begin {Главная программа}
{Подготовить начальную расстановку фишек}
repeat
{Цикл изменения условий игры}
Prepare; {Подготовить экран}
repeat {Игровой цикл}
GetPlayerMove; {Получить ход пользователя}
if not (exit or change) then
SetOwnerMove {Определить собственный ход} until exit or change
until exit
end.
В этом варианте главная программа содержит два вложенных друг в друга цикла
Repeat. . .Until: внутренний цикл управляет игрой, внешний отвечает за изменение
условий игры. Оба цикла управляются двумя логическими переменными, которые
являются глобальными для трех основных процедур PREPARE, GETPLAYERMOVE,
SETOWNERMOVE и, следовательно, могут изменяться внутри этих процедур.
Теперь настал момент подумать о том, каким способом в программе будет
храниться и использоваться информация о текущем состоянии игры. Судя по всему,
нам понадобятся хотя бы две переменные: в одной, назовем ее NROW, будет
содержаться число рядов фишек, в другой (NCOL) - количество фишек в каждом ряду.
Переменная NROW содержит одно целое положительное число, поэтому ее тип должен
быть INTEGER. В переменной NCOL должно быть не менее NROW целых чисел, т.е. ее
тип - это массив целых чисел. Поскольку в программе предусмотрена возможность
изменения условий игры самим игроком, переменная NROW может меняться от партии к
партии. В соответствии с этим должна была бы меняться и длина массива NCOL.
Однако в Турбо Паскале нельзя использовать массивы, длина которых меняется
динамически, т.е. в процессе работы программы. Эта длина должна определяться
статически (на этапе компиляции) и не может меняться в работающей программе.
Значит, понадобится массив достаточно большой длины, чтобы его хватило на все
случаи. На экране одновременно можно отобразить максимум 25 строк по 80 символов
в каждой строке. Однако использовать все строчки экрана как возможные ряды фишек
вряд ли целесообразно: во-первых, сама игра при большом количестве рядов
становится неинтересной, так как игрок не сможет проанализировать в уме все
варианты ходов; во-вторых, на экране не останется места для вывода другой
информации. Будем считать, что максимальное количество рядов фишек не должно
превышать 14. Укажем это константой MAXROW - теперь, если Вы захотите назначить
другое максимальное количество рядов, понадобится изменить значение этой
константы и перекомпилировать программу. Именно таким способом программам
придается дополнительная гибкость: Вы сосредоточиваете в нескольких константах
параметры, которые выбраны Вами произвольно и которые Вы или кто-то другой,
возможно, захочет изменить. Все размерности массивов или другие особенности
программной реализации следует определять через эти константы, тогда процедура
переделки программы предельно упростится.
С учетом сказанного назначим следующие глобальные константы и переменные:
const
MAXROW = 14; {Максимальное количество рядов}
MAXCOL = 20; {Максимальное
количество фишек в ряду}
type
ColType= array [I..MAXROW] of Integer;
var
exit :Boolean; {Признак окончания работы}
change:Boolean; {Признак изменения условий игры}
nrow :Integer; {Количество рядов}
ncol :ColType; {Максимальное колич-во фишек по рядам}
col :ColType; {Текущее количество фишек по рядам}
Константа MAXCOL не участвует в формировании массивов, она будет
использоваться для контроля горизонтальных размеров игрового поля. Поэтому она,
а также пять переменных сделаны глобальными. Если считать, что начальная
раскладка фишек соответствует схеме 3-4-5, то можно написать такой окончательный
вариант главной программы:
Uses CRT; {Подключение библиотеки дополнительных
процедур и функций для управления экраном}
const |
|
MAXROW = 14; |
{Максимальное количество рядов} |
MAXCOL =20; |
{Максимальное количество фишек в ряду} |
type |
|
ColType = array |
[1.. MAXROW] of Integer; |
var |
|
exit : Boolean; |
{Признак окончания работы} |
change : Boolean; |
{Признак изменения условий игры} |
nrow : Integer; |
{Количество рядов.} |
ncol : ColType; |
{Максимальное колич-во фишек по рядам} |
col : ColType; |
{Текущее количество фишек по рядам} |
{------------------------}
Procedure Prepare; {Готовит экран к игре}
begin {Prepare}
end; {Prepare}
{------------------------}
Procedure GetPlayerMove;
{Получает, контролирует и отображает ход игрока}
begin {GetPlayerMove}
end ; {Get PlayerMove}
{------------------------}
Procedure SetOwnerMove;
{Находит и отображает очередной ход программы}
begin {SetOwnerMove}
end; {SetOwnerMove}
{------------------------}
begin {Главная программа}
nrow := 3; {Готовим игру... }
ncol [1]:= 3; { на поле из трех }
ncol [2]:= 4;
{ рядов фишек }
ncol [3]:= 5; { по схеме 3-4-5.}
repeat {Цикл изменения условий
игры}
Prepare; {Подготовить экран}
repeat {Игровой цикл}
GetPlayerMove; {Получить ход пользователя}
if not (exit or change) then
SetOwnerMove {Определить собственный ход}
until exit or change
until exit
end.
Приступим к конструированию процедуры PREPARE. В ходе ее работы формируется
значение переменной COL, соответствующее начальной раскладке фишек, и выводится
информация о правилах игры. Чтобы было понятнее дальнейшее описание программной
реализации, на рис. 2.4 показан вид экрана в начальном состоянии игры.
Процедура начинает свою работу с очистки экрана от имеющейся на нем
информации. Это достигается обращением к стандартной процедуре без параметров
CLRSCR. Затем выводятся три строчки с названием игры и кратким описанием ее
правил. Кроме того, слева и справа на экране формируются заголовки для двух
колонок цифр, в которых затем будут отображаться номер ряда (слева) и текущее
количество фишек в ряду (справа). Эта информация поможет игроку сообщить
программе свой ход. Для размещения информации на нужных участках экрана
используется процедура GOTOXY(X,Y) , с помощью которой курсор перемещается
нужным образом. Параметры X и Y этой процедуры задают новые координаты курсора.
Начало координат соответствует точке (1,1) и размещается в левом верхнем углу
экрана, поэтому горизонтальная координата увеличивается слева направо, а
вертикальная - сверху вниз.
Рис.2.4. Вид экрана в начале игры ним
Procedure Prepare;
{Подготовка данных и экрана к игре}
const
Header0 = 'ИГРА НИМ';
Headerl = 'Вы можете взять любое число фишек из любого ряда.';
Header2 = 'Выигрывает тот, кто возьмет последнюю фишку.';
Headers = 'Номер ряда';
Header4 = 'Количество фишек';
var
i : Integer; begin {Prepare}
ClrScr; {Очищаем экран}
{Выводим строки заголовка:}
GotoXY((80-Length(Header0)) div 2,1);
Write(HeaderO) ;
GotoXY((80-Length(Headerl)) div 2,2);
Write(Headerl);
GotoXY((80-Length(Header2)) div2,3);
Writeln(Header2);
Write(Header3);
GotoXY(80-Length(Header4),4);
Write(Header4);
{Готовим
начальную раскладку:}
for i := 1 to nrow do
col [i] := ncol[i]
end; {Prepare}
Для вывода верхних строк строго посередине экрана используется задание
горизонтальной координаты курсора для процедуры GotoXY как половины от разницы
между полной длиной экрана (80 позиций) и длиной выводимой строки (определяется
с помощью функции LENGTH).
В процедуре GetPlayerMove осуществляются ввод, контроль и отображение на
экране очередного хода игрока. Предварительно нужно показать игроку текущее
состояние игрового поля. Поскольку поле будет обновляться как минимум дважды
(после хода игрока и после хода программы), действия, связанные с изображением
поля на экране, следует вынести в отдельную процедуру. Назовем ее ShowField и
займемся ее реализацией.
Судя по всему, нам понадобится организовать цикл; в ходе цикла для каждого
ряда игрового поля будет выведена строка, в левой части которой указывается
номер ряда, в правой - текущее количество фишек в нем, а посередине выводятся
символы, имитирующие фишки. В принципе, можно выбрать любой символ ПК для
обозначения фишки, например, X или О. Я предпочел воспользоваться символом
псевдографики с кодом 220: этот символ представляет собой небольшой квадратик и
легко ассоциируется с фишкой.
Procedure ShowField;
{ Отображает на экране текущее состояние игрового поля }
const
FISH = #220; {Символ-указатель фишки}
Х0 = 4; {Левая колонка номеров рядов}
X1 =72; {Правая колонка количества фишек}
X = 20; {Левый край игрового поля}
var
i,j : Integer;
begin {ShowField}
for i := 1 to nrow do begin
GotoXY(X0,i+4);
Write(i); {Номер ряда}
GotoXY(X1,i+4);
Write(col[i]:2); {Количество фишек в ряду}
for j := 1 to ncol[i] do {Вывод ряда фишек:}
begin
GotoXY(X+2*j,i+4); if j[i] then
Write(FISH)
else
Write('.')
end
end
end; {ShowField}
Символы FISH (квадратики) выводятся через одну позицию, чтобы не сливались на
экране. В те позиции, в которых ранее стояли уже снятые с поля фишки, выводится
точка.
Теперь вернемся к процедуре GETPLAYERMOVE. При вводе любого очередного хода
игрок должен задать два целых числа X1 и Х2. Первое из них указывает номер ряда,
а второе - количество фишек, которые игрок хочет забрать из этого ряда.
Программа должна проконтролировать правильность задания этих чисел: X1 должно
указывать непустой ряд, Х2 не может превышать количество фишек в этом ряду.
Кроме того, мы должны условиться о двух особых случаях:
пользователь больше не хочет играть и дает команду завершить работу
программы;
пользователь хочет изменить условия игры.
Пусть ввод числа X1 =0 означает команду выхода из программы, а X1 = -1 -
команду изменения условий игры. Тогда можно написать такой начальный вариант
процедуры:
Procedure GetPlayerMove;
{Получает, контролирует и отображает ход игрока}
var
correctly : Boolean; {Признак правильности сделанного хода}
xl,x2 : Integer;
{Вводимый ход}
begin {GetPlayerMove}
{Показываем начальное состояние игрового поля}
ShowField;
{Сообщаем, игроку правила ввода хода}
repeat
{Приглашаем игрока ввести ход}
ReadLn(xl,x2); {Вводим очередной ход}
exit := xl=0; {Контроль команды выхода}
change := xl=-l; {Контроль команды изменения}
if not (exit or change) then
{Проверить правильность хода и установить нужное значение переменной
CORRECTLY. Если ход правильный, сделать нужные изменения в раскладке фишек и
показать поле.}
else
correctly := true {Случай EXIT или CHANGE}
until correctly; if change then
{ Изменить условия игры }
end; {GetPlayerMove}
В этом варианте в процедуре GetPlayerMove нет описания процедуры SHOWFIELD.
Сделано это не случайно: процедура ShowField может понадобиться
также и при реализации процедуры SetOwnerMove, поэтому она должна быть
глобальной по отношению и к GetPlayerMove, и к SetOwnerMove, т.е. ее описание
должно в тексте программы предшествовать описаниям двух использующих ее процедур.
Действия
{ Сообщить игроку правила ввода хода } ,
{ Пригласить игрока ввести ход }
и
{Проверить правильность хода и установить нужное значение
переменной Correctly. Если ход правильный, сделать нужные
изменения в раскладке фишек и показать поле.}
не очень сложны в реализации, поэтому их можно осуществить непосредственно в
теле процедуры GETPLAYERMOVE. Иное дело - изменение условий игры. Это действие
полезно реализовать в отдельной процедуре GETCHANGE. С учетом этого второй
вариант процедуры GETPLAYERMOVE примет такой вид:
Procedure GetPlayerMove;
{Получает, контролирует и отображает ход игрока}
const
ТЕХТ1 = 'Введите Ваш ход в формате РЯД КОЛИЧ ';
ТЕХТ01= ' (например, 2 3- взять из 2 ряда 3 фишки) ' ;
ТЕХТ2 = 'или введите 0 0 для выхода из игры; ' ; .
ТЕХТ02= '-1 0 для
настройки игры';
ТЕХТЗ = 'Ваш ход:
';
Y = 20; {Номер строки для вывода сообщений}
var
correctly : Boolean; {Признак правильности сделанного хода}
xl,x2 : Integer; {Вводимый ход}
{-----------------}
Procedure GetChange;
{Устанавливает новую настройку игры (количество рядов и количество фишек в
каждом ряду}
begin {GetChange}
end; {GetChange}
{-----------------}
begin {GetPlayerMove}
ShowField; {Показываем начальное состояние поля}
{Сообщить игроку правила ввода хода:}
GotoXY((80-Length(TEXT1+TEXT01)) div2,Y);
Write(TEXT1+TEXT01);
GotoXY((80-Length(TEXT2+TEXT02)) div2,Y+l);
Write(TEXT2+TEXT02);
repeat
{Пригласить игрока ввести ход:}
GotoXY(l,Y+2);
Write(TEXTS); {Выводим приглашение и стираем предыдущий ход}
GotoXY(WhereX-16,Y+2); {Курсор влево на 16 позиций}
ReadLn(xl,x2); {Вводим очередной ход}
exit := xl=0; {Контроль команды выхода}
change := xl=-l; {Контроль команды изменения}
if not (exit or change) then
begin
correctly := (xl > 0) and (xl <= nrow) and
(x2 <= col[xl]) and (x2 > 0) ;
if correctly then
begin {Ход правильный:}
col[xl] := col[xl]-x2; {Изменяем раскладку фишек}
ShowField {Показываем поле}
end
else
Write(#7) {Ход неправильный: дать звуковой сигнал}
end
else
correctly := true {Случай EXIT или CHANGE}
until correctly;
if change then
GetChange end; {GetPlayerMove}
Обратите внимание: константа
ТЕХТЗ = 'Ваш ход:
имеет длинный «хвост» из пробелов (их 17), поэтому после вывода этого
приглашения курсор возвращается влево на 16 позиций оператором
GotoXY(WhereX-16,Y+2); {курсор влево на 16 позиций}
(функция WHEREX возвращает текущую горизонтальную координату курсора, а
функция WHEREY - его вертикальную координату). Сделано это для того, чтобы в
случае, если игрок ввел неверный ход и программа повторяет вывод приглашения,
пробелы в константе ТЕХТЗ затерли бы строку предыдущего ввода.
Чтобы завершить создание процедуры GETPLAYERMOVE, нужно спроектировать
процедуру GETCHANGE, в которой осуществляется изменение условий игры. Я привожу
текст этой процедуры без пояснений и приглашаю Вас самостоятельно разобраться в
том, как она работает:
Procedure GetChange;
{Устанавливает новую настройку игры (количество рядов и количество фишек в
каждом ряду}
const
tl='HACTPOЙKA ИГРЫ';
t2 ='(ввод количества рядов и количества
'+'фишек в каждом ряду)';
var
correctly : Boolean;
i : Integer; begin {GetChange}
ClrScr;
GotoXY( (80 -Length (tl) ) div2,l);
Write(tl) ;
GotoXY( (80 -Length (t2) ) div2,2);
Write (t2);
repeat
GotoXYd, 3) ;
Write ( 'Введите количество рядов (максимум ',
MAXROW, '):');
GotoXY(WhereX-6,WhereY) ;
ReadLn (nrow) ;
correctly := (nrow <= MAXROW) and (nrow > 1) ;
if not correctly then
Write (#7)
until correctly;
for i := 1 to nrow do
repeat
GotoXY(l,i+3) ;
Write (' ряд ',i,', количество фишек (максимум ',
MAXCOL , ' ) : ' ) ;
GotoXY (WhereX - 6 , WhereY) ;
ReadLn (ncol [i] ) ;
correctly := (ncol [i] <= MAXCOL) and (ncol[i] > 0) ;
if not correctly
then
Write (#7)
until correctly
end; {GetChange}
Переходим к конструированию процедуры SETOWNERMOVE, в которой программа
должна проконтролировать текущую ситуацию на игровом поле и выбрать собственный
ход. Работа процедуры начинается с подсчета числа непустых рядов. В зависимости
от этого подсчета реализуются следующие действия:
если все ряды пусты, значит предыдущим ходом игрок забрал последнюю фишку и
он победил; нужно поздравить его с победой, усложнить игру и предложить сыграть
еще раз;
если есть только один непустой ряд, то очередной ход программы очевиден
-забрать все фишки, что означает победу машины: сообщить об этом и предложить
сыграть еще раз;
если осталось два или более непустых ряда, выбрать собственный ход на
основе оптимальной стратегии. Начальный вариант процедуры:
Procedure SetOwnerMove;
{Находит и отображает очередной ход программы}
{-----------------}
Function CheckField : Integer;
{Проверяет состояния игры. Возвращает 0, если нет ни одной фишки (победа
игрока) , 1 - есть один ряд (победа машины) и - количество непустых рядов в
остальных случаях}
begin {CheckField}
end; {CheckField}
{-----------------}
Procedure PlayerVictory;
{Поздравить игрока с победой и усложнить игру}
begin {PlayerVictory}
end; {PlayerVictory}
{-----------------}
Procedure OwnVictory;
{Победа машины}
begin {OwnVictory}
end; {OwnVictory}
{-----------------}
Procedure ChooseMove;
{Выбор очередного хода}
begin {ChooseMove}
end; {ChooseMove}
{-----------------}
begin {SetOwnerMove}
case CheckField of {Проверяем количество непустых рядов}
0 : PlayerVictory; {Все ряды пусты - победа игрока}
1 : OwnVictory; {Один
непустой ряд - победа машины}
else
ChooseMove; {Выбираем очередной ход}
end; {case}
end; {SetOwnerMove}
Функция CHECKFIELD и процедуры PLAYERVICTORY и OWNVICTORY достаточно просты и
их текст помещается без каких-либо пояснений в окончательный вариант программы
(см. прил.5.3). Отмечу лишь, что в случае победы игрока нет смысла повторять
партию заново с той же самой раскладкой фишек. Поэтому игра усложняется: в
исходную раскладку добавляется еще по одной фишке в каждый ряд.
В процедуре CHOOSEMOVE анализируется позиция и выбирается очередной ход
программы. Описание оптимальной стратегии уже приводилось выше. Действия
программы заключаются в поиске первого слева (старшего) двоичного разряда, для
которого сумма чисел нечетная. Если такой разряд не обнаружен, то текущая
позиция безопасна для игрока, а значит любой ход программы сделает ее опасной. В
этом случае для программы не существует оптимального выбора и она лишь убирает
одну фишку из любого непустого ряда. Такая тактика означает пассивное ожидание
ошибки игрока.
Если обнаружен разряд i
с нечетной суммой, программа приступает к реализации
оптимальной стратегии и тогда игрок обречен на поражение. Для выбора ряда, из
которого следует взять фишки, программа просматривает последовательно все ряды и
отыскивает тот ряд j, количество фишек в котором (в двоичном представлении) дает
единицу в разряде i. Значение этого разряда для количества фишек в ряду
j
заменяется нулем. Затем программа продолжает подсчет суммы для оставшихся
младших разрядов. Если в каком-либо из них вновь обнаружена нечетность, значение
этого разряда для количества фишек в рядуj инвертируется, т.е.
0 заменяется на 1, а 1 на 0.
Например, если двоичные представления числа фишек и четности сумм таковы:
число фишек в ряду j: 01001
четность сумм: 01011
(единицей указаны разряды с нечетными суммами), то в результате этой операции
получим:
число фишек в ряду j: 00010
четность сумм: 00000
Таким образом, в исходном состоянии в ряду
j было 1001 =9 фишек, безопасная
позиция требует, чтобы в ряду осталось 0010 = 2 фишки, следовательно, из него
нужно забрать 9-2 = 7 фишек.
Окончательный вариант программы представлен в прил.5.3. Попробуйте
разобраться в ее деталях самостоятельно.
В программной реализации алгоритма широко используется то обстоятельство, что
Ваш компьютер, как и все остальные вычислительные машины, работает с числами,
представленными в двоичной системе счисления. Поэтому для получения двоичного
представления числа в процедуре BITFORM оно проверяется на четность с помощью
стандартной функции ODD, затем сдвигается вправо на один двоичный разряд
(операция SHR), вновь осуществляется проверка на четность и т.д. до тех пор,
пока не будут проверены все разряды. Максимальное число двоичных разрядов,
достаточное для двоичного представления количества фишек в ряду MAXCOL=63,
задается константой ВIТ=6.
Для получения суммы двоичных разрядов в процедуре CHOOSEMOVE используется
суммирование разрядов по модулю 2 с помощью операции XOR. Такое суммирование
дает 0, если количество единиц четное или равно нулю, и 1 - если нечетное. В
этой же процедуре для инверсии двоичного разряда применяется оператор
if nbit[i] = 1 then
ncbit[j,i] := ord(ncbit[j,i]=0); {Инверсия разрядов},
в котором используется соглашение о внутреннем представлении логических
величин в Турбо Паскале: 0 соответствует FALSE, а 1 - TRUE.