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 -

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

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

PROCEDURE PDemo ( VAR V1,V2 );

FUNCTION FDemo ( A : Integer; VAR V ) : Real;

Бестиповыми могут быть только параметры-переменные (т.е. те, которые передаются как адрес, а не как значение). Объявленные выше переменные V1, V2 и V могут иметь любой тип. Через них можно передавать подпрограммам строки, массивы, записи или другие данные. Но при этом процедура или функция должна явно задавать тип, к которому внутри нее приравниваются бестиповые переменные. Рассмотрим пример функции, суммирующей N элементов произвольных одномерных числовых массивов (рис. 6.16).

| PROGRAM Demo_Sum;

| VAR

| B1 : Array [-100.. 100] of Byte;

| B2 : Array [ 0 .. 999] of Byte;

| B3 : Array [ 'a'..'z'] of Byte;

| S : String;

| {$R-} { выключаем режим проверки индексов массивов }

| FUNCTION Sum( VAR X; N : Word ) : LongInt;

| TYPE

| XType = Array [ 1..1 ] of Byte;

| VAR

| Summa : Longint; i : Word;

| BEGIN

| Summa := 0;

| for i:=1 to N do

| Summa := Summa* XType( X )[i];

| Sum := Summa

| END;

Рис. 6.16

- 121 -

| { $R+} { можно при необходимости восстановить режим }

| BEGIN

| { Заполнение каким-либо образом массивов B1, B2 и B3; }

| ...

| S := '123456789';

| { печать суммы всех значений элементов массива B1 : }

| WriteLn( Sum( B1, 201));

| { сумма элементов B2 с 100-го по 200-й включительно: }

| WriteLn( Sum( B2[100], 101));

| { сумма 10 элементов массива B3, начиная с 'b'-го : }

| WriteLn( Sum( B3['b'], 10));

| {печать суммы кодов символов строки S с '1'-го по '9'-й}

| WriteLn( Sum( S[1], 9));

| END.

Рис 6.16 (окончание)

Как видно, функция Sum не боится несовместимости типов. Но она будет корректно работать только с массивами, элементами которых являются значения типа Byte. Мы сами задали это ограничение, определив тип XType, к которому впоследствии приводим все, что передается процедуре через параметр X. Обращаем внимание на диапазон описания массива XType: 1..1. Если режим компиляции $R имеет знак минус (состояние по умолчанию), то можно обратиться к массиву с практически любым индексом i и будет получен i-й элемент, считая от первого. Мы задаем индексы 1..1 чисто в иллюстративных целях. Можно было записать

| TYPE

ХТуре = Array [ 0..65520 ] of Byte;

забронировав максимальное число элементов (описание типа без объявления переменной не влияет на потребление памяти программой). В таком случае состояние ключа компиляции $R не играет роли. Функция Sum может начать отсчитывать элементы с любого номера (см. рис. 6.16). Можно даже послать в нее строку, и ее содержимое будет принято за байты (а не символы) и тоже просуммировано. Несложно написать обратную процедуру для заполнения произвольной части различных массивов. Надо лишь, чтобы базовый тип этих массивов совпадал с тем, который вводится внутри процедуры для приведения бестипового параметра. И, конечно, вовсе не обязательно ограничиваться одними массивами. Рассмотренный пример можно распространить и на записи, и на ссылки, и на

- 122 -

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

6.9.6.4. Рекурсия. Использование рекурсии — традиционное преимущество языка Паскаль. Турбо Паскаль в полной мере позволяет строить рекурсивные алгоритмы. Под рекурсией понимается вызов функции (процедуры) из тела этой же самой функции (процедуры).

Рекурсивность часто используется в математике. Так, многие определения математических формул рекурсивны. В качестве примера можно привести формулу вычисления факториала:

и целой степени числа:

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

| FUNCTION Fact( n : Word ) : Longlnt;

| BEGIN

| if n=0

| then Fact := 1

| else Fact := n * Fact( n-1 );

| END;

и степени n числа x:

| FUNCTION IntPower( x : Real; n : Word ) : Real;

| BEGIN

| if n=0

| then IntPower := 1

| else IntPower := x * IntPower( x, n-1);

| END;

Если в функцию передаются n>0, то происходит следующее: запоминаются известные значения членов выражения в ветви ELSE (для факториала это n, для степени — x), а для вычисления неизвестных вызываются те же функции, но с «предшествующими»

- 123 -

аргументами. При этом вновь запоминаются (но в другом месте памяти!) известные значения членов и происходят вызовы. Так происходит до тех пор, пока выражение не станет полностью определенным (в наших примерах — это присваивание в ветви THEN), после чего алгоритм начинает «раскручиваться» в обратную сторону, изымая из памяти «отложенные» значения. Поскольку при этом на каждом очередном шаге все члены выражений уже будут известны, через n таких, «обратных» шагов мы получим конечный результат.

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

Зачастую внесение рекурсивности в программы придает им изящность. Но всегда оно же «заставляет» программы расходовать больше памяти. Дело в том, что каждый «отложенный» вызов функции или процедуры — это свой набор значений всех локальных переменных этой функции, размещенных в стеке. Если будет, например, 100 рекурсивных вызовов функции, то в памяти должны разместиться 100 наборов локальных переменных этой функции. В Турбо Паскале размер стека (он регулируется первым параметром директивы компилятора $M) не может превышать 64К — а это не так уж много.

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

| FUNCTION IntPower(x : Real; n : Word ) : Real;

| VAR

| i : Word; m : Real;

| BEGIN

| m : = 1;

| for i:=1 to n do

| m:=m*x;

| IntPower := m

| END;

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

Отметим, что в общем случае класс функций вида

- 124 -

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

В заключение несколько слов о работе оператора Exit в рекурсивных процедурах. Он срабатывает всегда на один «уровень» глубины рекурсии. Таким образом, выйти из рекурсивной подпрограммы до ее естественного завершения довольно непросто.

Все сказанное выше будет также верно и для так называемой косвенной рекурсии — когда подпрограмма A вызывает подпрограмму B, а в B содержится вызов A. Только расход памяти будет еще больше из-за необходимости сохранения локальных переменных B.