Программирование в среде Турбо Паскаль - Дмитрий Поляков 13 стр.


Процедуры и функции могут быть вложенными друг в друга (см. рис. 6.5). Число уровней вложенности может быть достаточно большим, но на практике не превышает второго уровня. Вложенная процедура или функция относится к охватывающей ее подпрограмме точно так же, как сама подпрограмма относится к основной программе. Вложенные процедуры или функции могут вызываться только внутри охватывающей подпрограммы. Переменные, вводимые в них, будут по-прежнему локальными, а глобальными будут считаться все локальные переменные охватывающей подпрограммы и, как и ранее, все действительно глобальные переменные (а также типы и константы), объявленные в основной программе перед описанием подпрограмм.

Область действия меток переходов всегда локальна, и нельзя планировать переходы с помощью оператора Goto из вложенной, например, процедуры, в охватывающую или в основной блок программы, или из программы в процедуру.

- 112 -

6.9.2. Опережающее описание процедур и функций

Текст программы транслируется в выполнимый код последовательно сверху вниз. При этом переменные, типы, константы и подпрограммы должны описываться до того, как начнутся их упоминания в операторах или выражениях. В противном случае компилятор объявит имя неизвестным, и придется перемещать описания подпрограмм вверх по тексту программы. В случаях с процедурами и функциями этого можно избежать, используя опережающее описание директивой FORWARD:

PROCEDURE ИмяПроцедуры(параметры); FORWARD;

FUNCTION ИмяФункции( параметры ) : ТипЗначения; FORWARD;

......

PROCEDURE ИмяПроцедуры; { список параметров уже не нужен }

Тело процедуры

FUNCTION ИмяФункции; { достаточно указать только имя }

Тело функции

......

Эта директива объявляет заголовок подпрограммы, откладывая описание содержимого "на потом". Местоположение этого описания уже не играет роли, и в нем можно не указывать параметры, а ограничиться лишь именем подпрограммы. Основное описание не может иметь никаких директив (FORWARD, EXTERNAL и др.).

Директива FORWARD существует в языке в основном для развязки закольцованных вызовов. Так, ситуацию на рис. 6.9 можно разрешить только с ее помощью:

PROCEDURE a( у : TypeXY ); FORWARD;

PROCEDURE b( x : TypeXY );

BEGIN

...

a(p); {процедура b вызывает a}

END;

PROCEDURE a;

BEGIN

...

b( q ); {но сама a вызывает b }

END;

Рис. 6.9

- 113 -

6.9.3. Объявление внешних процедур

Турбо Паскаль - язык не слишком коммуникабельный по отношению к прочим языкам. Он не поддерживает генерацию объектных файлов в формате OBJ и вследствие этого не может поставлять написанные на нем процедуры и функции для связи с другими языками. Единственное, что можно - это использовать при компиляции и компоновке программ на Турбо Паскале внешние подпрограммы в виде OBJ-файлов, созданных другими компиляторами. OBJ-файлы должны при этом удовлетворять определенным требованиям к используемой модели памяти и способу передачи значений. Гарантируется совместимость кодов, полученных компилятором Turbo Assembler. He должно быть проблем и с кодами от ассемблера MASM или ему подобных. Возможен импорт объектных кодов, полученных в Turbo C и других языках, но на практике он труднореализуем.

Команды подстыковки объектных файлов в программу на Турбо Паскале задаются директивами компилятора {$L ИмяФайла.OBJ}, установленными в подходящих местах программы. А те процедуры и функции, которые реализованы в этих файлах, должны быть объявлены своими заголовками и специальным словом EXTERNAL, например:

{$L memlib.obj} { включение объектного кода }

procedure MemProc1; external;

PROCEDURE MemProc2( X,Y : Byte ); EXTERNAL;

FUNCTION MemFunc1( X :Byte; VAR Y :Byte ): Word; EXTERNAL;

Подключенные таким образом внешние функции или процедуры в дальнейшем ничем не отличаются от написанных в тексте. Обычно директиву включения OBJ-файла и объявления внешних подпрограмм удобно размещать рядом. Порядок следования директивы $L и описаний заголовков может быть произвольным.

6.9.4. Процедуры и функции как параметры

Отличительной особенностью Турбо Паскаля является разрешение передавать в процедуры и функции имена других подпрограмм, оформляя их как параметры. И точно так же, как передавалось значение, может передаваться некая функция его обработки. Особенно важным это становится при программной реализации алгоритмов вычислительной математики (хотя можно назвать и ряд других областей). Например, становится возможным написать процедуру интегрирования любой функции вида f(t) по следующей

- 114 -

схеме (рис. 6.10). Неочевидным здесь может показаться только введение функционального типа и то, как он определяется.

PROCEDURE Integrate LowerLimit, UpperLimit : Real;

VAR

Result : Real;

Funct : Функциональный тип);

VAR Описание локальных переменных процедуры

t : Real;

BEGIN

Численное интегрирование по t от LowerLimit до

Upper limit функции Funct, причем для получения

значения функции при заданном аргументе t достаточно

сделать вызов Funct(t).

Результат интегрирования должен быть возвращен через

параметр-переменную Result.

END;

Рис. 6.10

Функциональный или процедурный тип (в зависимости от того что описывается) - отнюдь не тип возвращаемого значения, а тип заголовка подпрограммы в целом. Так, на рис. 6.10 параметр Func есть одноместная функция вида f(t), возвращающая вещественное значение. Класс таких функций может быть описан типом

| TYPE

RealFunctionType = function ( t : Real ) : Real;

В этом описании имя подпрограммы не ставится - оно здесь не играет роли. Но обязательно перечисляются типы параметров и, если тип описывает функцию, тип результата. Идентификаторы параметров могут быть выбраны произвольно. Основная смысловая нагрузка падает на их типы и порядок следования. Тип, к которому могла бы принадлежать процедура Integral (см. рис. 6.10), должен был бы выглядеть примерно так:

| TYPE

ProcType = procedure ( А, В : Real; VAR X : Real;

f : RealFunctionType );

а тип процедуры без параметров:

NoParamProcType = procedure;

После объявления процедурного (функционального) типа его можно использовать в описаниях параметров подпрограмм. И, ко-

- 115 -

нечно, необходимо написать те реальные процедуры и функции, которые будут передаваться как параметры. Требование к ним одно: они должны компилироваться в режиме {$F+}. Поскольку по умолчанию принят режим {$F-}, такие процедуры обрамляются парой соответствующих директив. На рис. 6.11 дан пример функции, принадлежащей введенному выше типу RealFunctionType.

| { $F+} { включение режима $F+ }

| FUNCTION SinExp ( tt : Real ) : Real;

| BEGIN

| SinExp := Sin(tt)*Exp(tt)

| END;

| {$F-} { восстановление режима по умолчанию }

Рис. 6.11

Такая функция может быть подставлена в вызов подпрограммы на рис. 6.10:

Integral( 0, 1, Res1, SinExp )

и мы получим в переменной Res1 значение интеграла в пределах [0,1]. Не всякую функцию (процедуру) можно подставить в вызов. Нельзя подставлять: во-первых, процедуры с директивами inline и interrupt (из-за особенностей их машинного представления); во-вторых, вложенные процедуры или функции; в-третьих, стандартные процедуры и функции, входящие в системные библиотеки Турбо Паскаля. Нельзя, например, взять интеграл функции синуса:

Integral(0, 1, Res2, Sin)

хотя встроенная функция Sin внешне подходит по типу параметра. Последнее ограничение легко обходится переопределением функции (рис. 6.12).

| { $F+}

| FUNCTION Sin2( X : Real ) : Real;

| BEGIN

| Sin2 := Sin( X )

| END;

| {$F-}

Рис. 6.12

- 116 -

Теперь вызов процедуры интегрирования переписывается как

Integral( 0, 1, Res2, Sin2 )

Применение процедурного типа не ограничивается одним лишь описанием параметров-процедур или функций. Раз есть такой тип, то могут быть и переменные такого типа.

6.9.5. Переменные-процедуры и функции

Рассмотрим программу на рис. 6.13. В ней вводятся две переменные-процедуры P1 и P2 и демонстрируются возможные действия с ними.

| TYPE DemoProcType = procedure ( А,В : Word );

| VAR

| Р1, Р2 : DemoProcType; { переменные-процедуры} P : Pointer; { просто указатель }

| { Значения переменных-процедур : }

| {$F+}

| PROCEDURE Demo1( X,Y : Word );

| BEGIN WriteLn( 'x+y=', x+y ) END;

| PROCEDURE Demo2( X,Y : Word );

| BEGIN WriteLn( 'x-y=', x-y ) END;

| {$F-}

| BEGIN { основной блок программы }

| P1 := Demo1; { присваивание значений переменным }

| P2 := Demo2;

| P1( 1, 1 ); { то же самое, что и вызов Demo1(1,1) }

| P2( 2, 2 ); { то же самое, что и вызов Demo2(2,2) }

| { Ниже в указатель Р запишется адрес процедуры Р1: }

| DemoProcType( P ) := P1;

| DemoProcType(P)( 1, 1 ); { то же, что и вызов Р1(1,1) }

| { Так значение указателя Р передается переменной : }

| @P2 := Р;

| Р2( 2,2 ); { процедура Р2 в итоге стала равна Р1 }

| END.

Рис. 6.13

Процедурные переменные по формату совместимы с переменными типа Pointer и после приведения типов могут обмениваться с ними значениями. Для того чтобы переменная-процедура понималась как указатель на адрес подпрограммы в ОЗУ, она должна предваряться оператором @. Советуем не злоупотреблять операциями обмена значений таких переменных, тем более с приведениями типов. Програм-

- 117 -

мы с подобными приемами очень трудно отлаживать, и они имеют тенденцию "зависать" при малейшей ошибке.

Одной из причин зависания может стать очень распространенная ошибка: попытка использовать переменную-процедуру, не присвоив ей соответствующего значения. Эта ошибка не диагностируется ничем и приводит к непредсказуемым последствиям.

Пример на рис. 6.13 приведен, в общем-то, более для наглядности. Нет необходимости вводить переменные-процедуры или функции, ибо вместо них можно всегда подставить обычные вызовы. Но совсем другое дело, если переменная-процедура является частью какой-либо структуры, например записи:

TYPE

ProcType = ПроцедурныйИлиФункциональныйТип;

DemoRecType = RECORD

X,Y : Word;

Op : ProcType;

END;

VAR

Rec1,Rec2 : DemoRecType;

Используя такие структуры, можно хранить в них не только данные, но и процедуры их обработки. Причем в любой момент можно сменить процедуру или функцию, понимаемую под полем Op.

Обращаем внимание на еще одну особенность работы с процедурными переменными. Если надо убедиться, что процедуры или функции, понимаемые под двумя переменными, одинаковы, то операция сравнения запишется (для переменных Rec1.Op и Rec2.Op) как

IF @Rec1.Op = @Rec2.Op then ... ;

Если убрать оператор @, то при значениях полей Op, соответствующих процедурам, это будет просто синтаксически неверно, а при значениях Op, соответствующих функциям без параметров, будут сопоставляться не сами поля Op, а результаты вызовов функций.

6.9.6. Специальные приемы программирования

6.9.6.1. Обмен данными между подпрограммами через общие области памяти. Процедуры и функции могут модифицировать внешние переменные двумя способами: через свои параметры или непосредственным обращением к глобальным идентификаторам. В последнем случае вполне возможна ситуация, когда несколько подпрограмм модифицируют одну и ту же глобальную переменную. Можно пойти еще дальше и объявить в подпрограммах области данных, совмещенных со значением той же глобальней переменной.

- 118 -

Это, во-первых, освобождает нас от обязательства использовать везде один и тот же идентификатор, а во-вторых, позволяет при необходимости менять структуру обращения к данным. Пример подобной организации подпрограмм дан на рис. 6.14.

| TYPE

| Vector100Type = Array[1..100] of Real; {вектор }

| MatrixType = Array[1..10,1..10] of Real; { матрица }

| Matrix2Type = Array[1..50,1..2 ] of Real; { матрица }

| VAR

| V : Vector100Туре: { область памяти на 100 элементов }

| PROCEDURE P1;

| VAR M : MatrixType absolute V; { M совмещается с V }

| BEGIN

| В процедуре возможны обращения M[ i,j ], эквивалентные

| обращениям V[(i-1)*10+j]

| END;

| PROCEDURE P2;

| VAR M2 : Matrix2Type absolute V; { M2 совмещается с V }

| BEGIN

| В процедуре возможны обращения M2[ i,j ], эквивалентные

| обращениям V(i-1)*2+j]

| END;

| PROCEDURE P3;

| VAR V3 : Vector100Type absolute V; {V3 совмещается с V}

| BEGIN

| Обращения V3[i] в процедуре эквивалентны обращениям V[i]

| END;

| BEGIN

| Основной блок, содержащий вызовы P1, P2, P3 и, может быть,

| обращения к общей переменной (области памяти) V

| END.

Рис. 6.14

Здесь процедуры имеют доступ к одной и той же области данных (т.е. к ста вещественным значениям), но осуществляют его разными методами. Поскольку нельзя совмещать значения локальных переменных с локальными, как минимум одна переменная из разделяющих общую область памяти должна быть глобальной. В примере на рис. 6.14 это переменная V.

- 119 -

Описанный выше прием программирования аналогичен, по сути, объявлению общих блоков в языке Фортран и во многих случаях позволяет составлять компактные и эффективные программы.

6.9.6.2. Статические локальные переменные. Обыкновенные локальные переменные в подпрограммах всегда "забывают" свое значение в момент окончания работы соответствующей подпрограммы. А при повторном вызове стартовые значения локальных переменных совершенно случайны. И если надо сохранять от вызова к вызову какую-нибудь локальную информацию, то ни в коем случае нельзя полагаться на локальные переменные, описанные в разделах VAR процедур и функций или как параметры-значения в заголовках. Для сохранности между вызовами информация должна храниться вне подпрограммы, т.е. в виде значения глобальной переменной (переменных). Но в этом случае приходится отводить глобальные переменные, по сути, под локальные данные. Турбо Паскаль позволяет решать эту проблему, используя статические локальные переменные или, что то же самое, локальные переменные со стартовым значением. Они вводятся как типизированные константы (рис. 6.15) по тем же правилам, что и их глобальные аналоги (см. разд. 5.2.2).

| PROCEDURE XXXX( ...);

| VAR ... { обычные локальные переменные }

| CONST { статические локальные переменные }

| A : Word = 240;

| B : Real = 41.3;

| ARR : Array[-1..1] of Char=('ф', 'х', 'ц');

| BEGIN

| Тело процедуры, в котором могут изменяться значения

| A, B, Arr и других переменных

| END.

Рис. 6.15

Особенность переменных, объявленных таким образом, заключается в том, что, хотя по методу доступа они являются строго локальными, свои значения они хранят вместе с глобальными переменными (в сегменте данных). Поэтому значения переменных A, B и Arr на рис. 16.15 сохранятся неизменными до следующего вызова процедуры и после него. В них можно накапливать значения при многократных обращениях к процедурам или функциям, их можно использовать как флаги каких-либо событий и т.п.

- 120 -

Впрочем, эта особенность реализации языка может привести и к скрытым ошибкам. Из изложенного следует, что инициализация статической переменной стартовым значением происходит лишь один раз: при первом вызове подпрограммы. И если впоследствии значение этой переменной изменится, то восстановления стартового значения уже не произойдет. Поэтому будет ошибкой считать локальные типизированные константы действительно константами. Настоящими константами будут являться лишь простые константы.

Назад Дальше