6.7. Оператор безусловного перехода Goto
Оператор Goto, имеющийся во многих алгоритмических языках, включая и Турбо Паскаль, можно с полным правом назвать "злосчастным". В разгар моды на структурное программирование он подвергся сильным гонениям, ибо было доказано, что можно вообще программировать без него. Из-за того же Goto здорово доставалось Фортрану и раннему Бейсику, большей частью справедливо, так как от пары десятков переходов вдоль и поперек программа не становится более понятной.
Но на практике выбор между чистотой идеи (структурное программирование) и элементарным удобством (использование Goto) был предрешен, и в структурном Паскале мы можем использовать безусловные переходы по программе.
Полный синтаксис оператора перехода - это
Goto Метка;
- 104 -
где метка - описанный в блоке LABEL идентификатор (цифры от 0 до 9999 или собственно идентификатор). Метка может стоять в программе "где угодно" между операторами. При этом каждая метка может появиться только один раз (рис. 6.3):
| LABEL
| m10, m20, StopLabel, 1;
| VAR
| i : ShortInt;
| BEGIN
| 0001:
| IF i<10 THEN Goto m10 ELSE Goto m20;
| ...
| m10 : WriteLn('i меньше 10');
| Goto StopLabel;
| m20 : i := i - 1;
| ...
| Goto 1;
| StopLabel:
| END.
Рис. 6.3
Метка от оператора должна отделяться символом ":". Если метка обозначается цифрой, то предшествующие нули не являются значащими. Так, на рис. 6.3 метки 1 и 0001 эквивалентны. Обычно метки размещаются у операторов, но могут стоять и у слова END, что означает переход на конец текущего блока BEGIN...END (на рисунке это еще и выход из программы). Перед BEGIN метки не ставятся. Следует избегать переходов (и расстановки меток), передающих управление внутрь составных операторов циклов, да и вообще переходов в составные операторы, вложенные в тот, где выполняется оператор Goto. Другое дело - выход из вложенных операторов во внешние. В таких случаях применение Goto - достаточно безопасный и максимально эффективный способ вернуть управление внешнему оператору.
Область действия операторов перехода и связанных с ними меток строго локализована. Метки, описанные вне процедур или функций, имеют своей областью действий только основной блок программы. Но метки, описанные внутри определения процедур или функций, имеют смысл только внутри них, поэтому запрещены переходы по Goto между процедурами и между процедурами и основным блоком.
- 105 -
При практическом программировании на Паскале необходимость в использовании оператора Goto возникает не часто (если, конечно, не писать стилем Фортрана или Бейсика на Паскале). Иногда один переход позволяет избежать очень широких циклов, но злоупотребление переходами не будет признаком высокой культуры программирования.
6.8. Операторы Exit и Halt
Турбо Паскаль обладает средствами безусловного выхода из программных блоков (процедур, функций, основного блока программы). Это очень удобно, так как позволяет завершить программу или процедуру без предварительных переходов по меткам. К таким операторам завершения относятся вызовы системных процедур Exit и Halt.
Вызов Exit завершает работу своего программного блока. Если выполняется Exit в процедуре, то выполнение ее завершится и ход программы вернется к следующему за вызовом этой процедуры оператору. При этом процедура вернет значения, которые успели вычислиться к моменту выполнения Exit (если она должна их возвратить). Сама программа не прервется. Но если Exit выполняется в основном блоке программы, выход из него будет эквивалентен нормальному ее завершению. Процедура Exit - в некотором роде избыточная. Ее действие полностью эквивалентно безусловному переходу (Goto) на метку, стоящую перед последним словом END содержащей ее процедуры, функции или основного блока. Но использование Exit позволяет избежать лишней возни с метками и улучшает читаемость программ. Таким образом, Exit - это средство выхода из программного блока, а не из составного оператора, например цикла FOR. Вызов Exit может быть в трижды вложенном цикле процедуры, но его действие все равно будет относится к процедуре, как к программному блоку.
Процедура Halt, или более полно Halt(n), действует более грубо и менее разборчиво. Независимо от того, где она находится, ее выполнение завершает работу программы с кодом завершения n. Этот код впоследствии может быть проанализирован, в частности, командой IF ERRORLEVEL в среде MS-DOS. Значение ERRORLEVEL после остановки программы будет равно значению n. Значение n=0 соответствует нормальному коду завершения. Вызов процедуры Halt без параметра эквивалентен вызову Halt(0).
- 106 -
На основе процедуры Halt можно легко построить программу, например ASK.PAS, для организации диалога в ВАТ-файлах MS-DOS (рис. 6.4).
| VAR i : Word; { ======ПРОГРАММА ASK.PAS ======== }
| BEGIN
| { ...вывод на экран текста альтернатив выбора... }
| Write( 'Введите Ваш выбор: ');
| ReadLn(i); { ввод номера альтернативы с экрана)
| Halt(i) { остановка программы и назначение }
| END. { ERRORLEVEL в MS-DOS номера i }
Рис. 6.4
Теперь в ВАТ-файле надо запускать откомпилированную программу ASK.EXE и сразу после нее анализировать, что будет находиться в переменной MS-DOS ERRORLEVEL.
Имеет смысл при нескольких вызовах Halt в тексте программы назначать им разные коды завершения. Тогда можно будет при отладке или работе определить, чем вызвано прерывание программы.
6.9. Процедуры и функции
В этом разделе будут рассмотрены вопросы, связанные с написанием и употреблением подпрограмм, представленных в виде процедур или функций.
Определить простейшую процедуру довольно просто: практически любой составной оператор, вынесенный из основного блока программы и объявленный предложением
PROCEDURE ИмяПроцедуры;
становится процедурой, и вместо этого составного оператора в основном блоке может подставляться одно лишь ИмяПроцедуры.
Согласно более общему определению процедура может иметь параметры, метки перехода внутри себя и свои, локальные, переменные (рис. 6.5). Обязательными элементами процедур и функций тоже является заголовок и тело, т.е. тот же составной оператор.
Синтаксис вызова процедуры прост. Ее выполнение активизируется указанием ее имени и списком переменных или значений, подставляемых на место параметров:
ИмяПроцедуры(Параметр1, Параметр2,);
- 107 -
PROCEDURE ИмяПроцедуры (ПарамЗнач1 : ТипЗнач1;
ПарамЗнач2 : ТипЗнач2;
VAR ПарамПерем1 : ТипПерем1;
VAR ПарамПерем2 : ТипПерем2; ... );
LABEL
Перечисление меток внутри тела процедуры
CONST
Описание локальных констант процедуры
TYPE
Описание локальных типов
VAR
Описание локальных переменных
Описание вложенных процедур и (или) функций
BEGIN
Тело процедуры
END;
Рис. 6.5
Общая структура функций совпадает с процедурами, за исключением заголовка. Он записывается как
FUNCTION ИмяФункции( Список параметров ) :
ИмяСкалярногоТипаЗначенияФункций;
Что и как может возвращать функция при ее вызове, мы рассмотрим чуть позже.
Нетрудно заметить, что структура подпрограмм копирует структуру программы в целом (не считая заголовка и завершающей точки с запятой вместо точки после END). Порядок следования разделов описаний подчиняется тем же правилам, по которым строится вся программа.
6.9.1. Параметры. Глобальные и локальные описания
Поскольку процедуры и функции должны обладать определенной независимостью в смысле использования переменных (а также типов и констант), при их введении в программу возникает разделение данных и их типов на глобальные и локальные. Глобальные константы, типы, переменные - это те, которые объявлены в программе вне процедур или функций. Наоборот, локальные - это константы, типы и переменные, существующие только внутри процедур или функций, и объявленные либо в списке параметров (только переменные), либо в разделах CONST, TYPE, VAR внутри процедуры или функции.
- 108 -
Процедуры и функции могут, наряду со своими локальными данными, использовать и модифицировать и глобальные. Для этого нужно лишь, чтобы описание процедуры (функции) стояло в тексте программы ниже, чем описания соответствующих глобальных типов, констант и переменных (рис. 6.6).
PROGRAM Main;
| VAR
| Xmain, Ymain : LongInt; {глобальные переменные}
| Res : Real;
| PROCEDURE Proc1( a,b : Word; VAR Result : Real );
| VAR
| Res : Real; { локальная Res, закрывающая глобальную }
| BEGIN
| Res := a*a + b*b; { локальные действия }
| Result:= Xmain+Ymain*Res; {работают глобальные значения}
| Xmain := Xmain+1; { модифицируется глобальное значение}
| END;
TYPE
CONST Другие глобальные объявления, уже
VAR недоступные из процедуры Proc1;
BEGIN
Основной блок, в котором может вызываться Proc1
END.
Рис. 6.6
При совпадении имен локальной и глобальной переменных (типов, констант) сильнее оказывается локальное имя, и именно оно используется внутри подпрограммы. Так, существует неписанное правило: если подпрограмма содержит в себе циклы FOR, то параметры циклов должны быть описаны как локальные переменные. Это предупредит неразбериху при циклическом вызове процедур.
Мы уже отмечали, что параметры, описываемые в заголовке процедур и функций, по сути, являются локальными переменными. Но кроме того, они обеспечивают обмен значениями между вызывающими и вызываемыми частями программы (т.е. теми же процедурами или функциями). Описываемые в заголовке объявления подпрограммы параметры называются формальными, а те, которые подставляются на их место при вызове, - фактическими, ибо они при выполнении как бы замещают все вхождения в подпрограмму своих формальных "двойников".
- 109 -
Параметры подпрограмм разделяются на параметры-значения и параметры-переменные. Параметры-значения - это локальные переменные подпрограммы, стартовые значения которых задаются при вызове подпрограммы из внешних блоков (а те локальные переменные, которые описаны в разделе VAR между заголовком и телом подпрограммы, должны получать свои значения присваиванием внутри тела подпрограммы). Параметры-значения, описанные в заголовке, могут изменять свои значения наряду с прочими переменными, но эти изменения будут строго локальными и никак не передадутся в вызывающие операторы. Для того чтобы подпрограмма изменила значение переданной ей переменной, нужно объявлять соответствующие параметры как параметры-переменные, вставляя слово VAR перед их описанием в заголовках. Рассмотрим внутренний механизм передачи параметров подпрограмм. При вызове процедуры или функции каждой локальной переменной, описанной внутри процедуры, и каждому параметру-значению отводится место для хранения данных в специальной области памяти, называемой стеком. Эти места принадлежат переменным ровно столько времени, сколько выполняется подпрограмма. Причем ячейки, соответствующие параметрам-значениям, сразу заполняются конкретным содержимым, заданным в вызове подпрограммы. По-другому организуются параметры-переменные. Вместо копии значения подпрограмма получает разрешение работать с тем местом, где постоянно (т.е. все время работы самого вызывающего программного блока) хранится значение переменной, указанной в вызове на месте параметра-переменной. Все действия с параметром-переменной в подпрограмме на самом деле являются действиями над подставленной в вызов переменной.
В этом, кстати, заключается причина того, что на место параметров-значений можно подставлять непосредственно значения, а на местах параметров-переменных может быть лишь идентификатор переменной.
Рассмотрим пример процедуры (рис. 6.7), принимающей любое число и возвращающей его квадрат. Если значение квадрата числа превышает значение 100, то оно считается равным 100. При этом должен устанавливаться глобальный "флаг".
На рис. 6.7 отмечены оба способа обмена данными с процедурой: непосредственной модификацией глобальных переменных и передачей переменной через VAR-параметр. Обратите внимание на использование локальной переменной X. Подобные приемы иногда позволяют не вводить лишних локальных переменных.
- 110 -
| VAR
| GlobalFlag : Boolean; {глобальный флаг}
| PROCEDURE GetSQR( X : Real; VAR Sq : Real );
| { процедура не имеет локальных переменных, кроме X }
| CONST
| SQRMAX =100; { локальная простая константа }
| BEGIN { начало тела процедуры }
| { В X запишется квадрат его последнего значения: }
| X := X * X;
| { Результат сравнения запишется в глобальный флаг: }
| GlobalFlag := ( X > SQRMAX );
| if GlobalFlag then
| X:=SQRMAX; { ограничение X }
| Sq := X { возвращение значения }
| END; { конец тела процедуры }
| VAR
| SqGlobal : Real;
| BEGIN { основной (вызывающий) блок }
| GetSQR ( 5, SqGlobal );
| WriteLn( SqGlobal, ' Флаг: ', GlobalFlag )
| END.
Рис. 6.7
Оставим ненадолго процедуры и рассмотрим функции. Идентификатор функции возвращает после вызова скалярное значение заданного типа. Для присвоения функции значения ее имя должно хотя бы однажды появиться в левой части оператора присваивания в теле самой функции. Вызов функции производится уже не обособленно, а в том месте, где необходимо значение функции (в выражениях, вызовах других подпрограмм и т.п.). Например, процедуру GetSQR на рис. 6.7 можно переписать в виде функции (рис. 6.8).
| VAR GlobalFlag : Boolean; { глобальный флаг }
| FUNCTION GetSQR( X : Real ) : Real;
| CONST SQRMAX =100;
| BEGIN
| X := X*X;
| GlobalFlag:=(X>SQRMAX);
| if GlobalFlag then X:=SQRMAX;
| GetSQR := X { возвращение значения }
| END;
Рис. 6.8
- 111 -
| BEGIN { основной (вызывающий) блок }
| WriteLn( GetSQR( 5 ), ' Флаг: ', GlobalFlag )
| END.
Рис. 6.8 (окончание)
Вызов заметно упростился, не говоря уже о сокращенной переменной SqGlobal. Возвращаемое функцией значение должно быть скалярного или строкового типа, т.е. не может быть файлом, массивом, записью, множеством, объектом.
Функция, как и процедура, может обмениваться значениями с программой и изменять глобальные переменные непосредственно или через параметры-переменные. Обычно, когда функция, кроме выдачи своего значения, меняет какие-либо глобальные значения или производит другие действия, не связанные с вычислениями своего значения, говорят, что она имеет побочный эффект.
Большое значение имеет соблюдение правил соответствия типов при подстановке параметров. Нельзя (да и не получится - компилятор не пропустит!) конструировать типы в описаниях параметров. Можно использовать только уже известные идентификаторы типов. То же самое можно сказать о типе возвращаемого значения функции. И, конечно, число и порядок следования параметров в вызове должен соответствовать описанию процедуры или функции. Кроме того, в Турбо Паскале существует правило, требующее, чтобы параметры, имеющие файловый тип (или сложный тип с файловыми компонентами), были обязательно описаны как VAR-параметры.