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.