Смекни!
smekni.com

Основы программирования в паскале (стр. 44 из 44)

R:=aR;

end; Procedure TCircle.Draw;

Begin

SetColor(aColor);

Circle(X,Y,R);

end; Procedure TRect.Draw;

Begin

SetColor(aColor); Rectangle(X,Y,X+dX,Y+dY); end;


21


ПРОЦЕДУРЫ И ФУНКЦИИ в PASCAL


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

Упоминание этого имени в тексте программы называется вызовом процедуры (функции).

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

Условимся да­лее называть процедуру или функцию общим именем «подпрограмма», если только для излагаемого материала указанное отличие не имеет значения.

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

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

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

Вторая причина заключается в применении методики нисходящего проектирования программ (см. гл.2).

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

Последователь­ное структурирование программы продолжается до тех пор, пока реализуемые под­программами алгоритмы не станут настолько простыми, чтобы их можно было легко запрограммировать.

В этой главе подробно рассматриваются все аспекты использования подпрограмм в Турбо Паскале.


8.1. ЛОКАЛИЗАЦИЯ ИМЕН

Напомню, что вызов подпрограммы осуществляется простым упоминанием имени процедуры в операторе вызова процедуры или имени функции в выражении.

При ис­пользовании расширенного синтаксиса Турбо Паскаля (см. ниже) функции можно вызывать точно так же, как и процедуры.

Как известно, любое имя в программе долж­но быть обязательно описано перед тем как оно появится среди исполняемых операто­ров.

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

Описать подпрограмму - это значит указать ее заголовок и тело. В заголовке объ­являются имя подпрограммы и формальные параметры, если они есть.

Для функции, кроме того, указывается тип возвращаемого ею результата.

За заголовком следует тело подпрограммы, которое, подобно программе, состоит из раздела описаний и раздела исполняемых операторов.

В разделе описаний подпрограммы могут встретиться опи­сания подпрограмм низшего уровня, в тех - описания других подпрограмм и т.д.

Вот какую иерархию описаний получим, например, для программы, структура ко­торой изображена на риc.8.1 (для простоты считается, что все подпрограммы представляют собой процедуры без параметров):

Program ... ;

Procedure A;

Procedure А1;

…..

begin

..

end {A1};

Procedure A2;

…..

begin

..

end {A2}

begin {A}

…..

end {A}.


Procedure В;

Procedure B1;

..

begin

..

end {Bl};

Procedure В 2;

Procedure B21;

……

и т.д.


Подпрограмма любого уровня имеет обычно множество имен констант, перемен­ных, типов и вложенных в нее подпрограмм низшего уровня.

Считается, что все име­на, описанные внутри подпрограммы, локализуются в ней, т.е. они как бы «невидимы» снаружи подпрограммы.

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

Все детали этой реализации скрыты от глаз пользователя под­программы и потому недоступны ему.

Например, в рассмотренном выше примере из основной программы можно обратиться к процедурам А и В, но нельзя вызвать ни одну из вложенных в них процедур A1, A2, B1 и т.д.

Сказанное относится не только к именам подпрограмм, но и вообще к любым име­нам, объявленным в них - типам, константам, переменным и меткам.

Все имена в пре­делах подпрограммы, в которой они объявлены, должны быть уникальными и не мо­гут совпадать с именем самой подпрограммы.

При входе в подпрограмму низшего уровня становятся доступными не только объ­явленные в ней имена, но и сохраняется доступ ко всем именам верхнего уровня.

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

Так, например, из подпрограммы В21 мы можем вызвать подпрограмму А, использовать имена, объявленные в основной про­грамме, в подпрограммах В и B2, и даже обратиться к ним.

Любая подпрограмма мо­жет, наконец, вызвать саму себя - такой способ вызова называется рекурсией.

Пусть имеем такое описание:

Program .. ;

var V1 : ... ;

Procedure A;

var V2 : . . .;

..

end{A};

Procedure B;

var V3 : . . . ;

Procedure B1;

var V4 : .. . ;

Procedure В 11;

var V5;

.


Из процедуры В11 доступны все пять переменных V1,...,V5, из процедуры В1 дос­тупны переменные V1,…, V4, из центральной программы - только VI.

При взаимодействии подпрограмм одного уровня иерархии вступает в силу основ­ное правило Турбо Паскаля: любая подпрограмма перед ее использованием должна быть описана.

Поэтому из подпрограммы В можно вызвать подпрограмму А, но из А вызвать В невозможно (точнее, такая возможность появляется только с использовани­ем опережающего описания, см. п.8.6.)

Продолжая образное сравнение, подпрограмму можно уподобить ящику с непрозрачными стенками и дном и полупрозрачной кры­шей: из подпрограммы можно смотреть только «вверх» и нельзя «вниз», т.е. подпро­грамме доступны только те объекты верхнего уровня, которые описаны до описания данной подпрограммы.

Эти объекты называются глобальными по отношению к под­программе.

В отличие от стандартного Паскаля в Турбо Паскале допускается произвольная по­следовательность описания констант, переменных, типов, меток и подпрограмм.

На­пример, разделVAR описания переменных может появляться в пределах раздела опи­саний одной и той же подпрограммы много раз и перемежаться с объявлениями дру­гих объектов и подпрограмм.

Для Турбо Паскаля совершенно безразличен порядок следования и количество разделовVAR, CONST, TYPE, LABEL, но при определении области действия этих описаний следует помнить, что имена, описанные ниже по тек­сту программы, недоступны из ранее описанных подпрограмм, например:

var V1 : . . . ;

Procedure S;

var V2 : . . . ;

……

end {S};

var V3 : . . . ;

…..


Из процедуры S можно обратиться к переменным VI и V2, но нельзя использовать VЗ, так как описание V3 следует в программе за описанием процедуры S.

Имена, локализованные в подпрограмме, могут совпадать с ранее объявленными глобальными именами.


В этом случае считается, что локальное имя «закрывает» гло­бальное и делает его недоступным, например:

var

i : Integer;

Procedure P;

var

i : Integer;

begin

writeln(i)

end {P};

begin

i := 1;

P

end.

Что напечатает эта программа? Все, что угодно: значение внутренней переменной I при входе в процедуру Р не определено, хотя одноименная глобальная переменная имеет значение 1.

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

Если убрать описание

var

i : Integer;

из процедуры Р, то на экран будет выведено значение глобальной переменной I, т.е. 1.

Таким образом, одноименные глобальные и локальные переменные - это разные переменные.

Любое обращение к таким переменным в теле подпрограммы трактуется как обращение к локальным переменным, т.е. глобальные переменные в этом случае попросту недоступны.


8.2. ОПИСАНИЕ ПОДПРОГРАММЫ

Описание подпрограммы состоит из заголовка и тела подпрограммы.


8.2.1. Заголовок

Заголовок процедуры имеет вид:

PROCEDURE <имя> [ (<сп.ф.п.>) ];

Заголовок функции:

FUNCTION <имя> [(<сп.ф.п.»] : <тип>;

Здесь <имя> - имя подпрограммы (правильный идентификатор);

<сп. ф. п. >- список формальных параметров;

<тип> - тип возвращаемого функцией результата.


Сразу за заголовком подпрограммы может следовать одна из стандартных дирек­тивASSEMBLER, EXTERNAL, FAR, FORWARD, INLINE, INTERRUPT, NEAR.

Эти директивы уточняют действия компилятора и распространяются на всю подпрограмму и только на нее, т.е. если за подпрограммой следует другая подпрограмма, стандарт­ная директива, указанная за заголовком первой, не распространяется на вторую.

ASSEMBLER - эта директива отменяет стандартную последовательность машин­ных инструкций, вырабатываемых при входе в процедуру и перед выходом из нее. Тело подпрограммы в этом случае должно реализоваться с помощью команд встроен­ного ассемблера (см. п. 11.8).

EXTERNAL - с помощью этой директивы объявляется внешняя подпрограмма (см. п.11.1).

FAR - компилятор должен создавать код подпрограммы, рассчитанный на даль­нюю модель вызова.

ДирективаNEAR заставит компилятор создать код, рассчитан­ный на ближнюю модель памяти. По умолчанию все подпрограммы, объявленные в интерфейсной части модулей, генерируются с расчетом на дальнюю модель вызова, а все остальные подпрограммы - на ближнюю модель.

В соответствии с архитектурой микропроцессора ПК, в программах могут исполь­зоваться две модели памяти: ближняя и дальняя.

Модель памяти определяет возмож­ность вызова процедуры из различных частей программы:

если используется ближняя модель, вызов возможен только в пределах 64 Кбайт (в пределах одного сегмента ко­да, который выделяется основной программе и каждому используемому в ней моду­лю);

при дальней модели вызов возможен из любого сегмента.

Ближняя модель эконо­мит один байт и несколько микросекунд на каждом вызове подпрограммы, поэтому стандартный режим компиляции предполагает эту модель памяти.

Однако при переда­че процедурных параметров (см.п.8.4), а также в оверлейных модулях (см. п. 11.6) соответствующие подпрограммы должны компилироваться с расчетом на универсаль­ную - дальнюю - модель памяти, одинаково пригодную при любом расположении процедуры и вызывающей ее программы в памяти.

Явное объявление модели памяти стандартными директивами имеет более высокий приоритет по сравнению с опциями настройки среды Турбо Паскаля.

FORWARD - используется при опережающем описании (см. п.8.6) для сообщения компилятору, что описание подпрограммы следует где-то дальше по тексту програм­мы (но в пределах текущего программного модуля).

INLINE - указывает на то, что тело подпрограммы реализуется с помощью встро­енных машинных инструкций (см. п. 11.2).

INTERRUPT - используется при создании процедур обработки прерываний (см. п. 11.4).

8.2.2. Параметры

Список формальных параметров необязателен и может отсутствовать. Если же он есть, то в нем должны быть перечислены имена формальных параметров и их типы, например:


Procedure SB(a: Real; b: Integer; c: Char) ;

Как видно из примера, параметры в списке отделяются друг от друга точками с за­пятой. Несколько следующих подряд однотипных параметров можно объединять в подсписки, например, вместо


Function F(a: Real; b: Real): Real;


можно написать проще:


Function F(a,b: Real): Real;


Операторы тела подпрограммы рассматривают список формальных параметров как своеобразное расширение раздела описаний: все переменные из этого списка могут использоваться в любых выражениях внутри подпрограммы.

Таким способом осуще­ствляется настройка алгоритма подпрограммы на конкретную задачу.

Рассмотрим следующий пример. В языке Турбо Паскаль нет операции возведения в степень, однако с помощью встроенных функций LN(X) и ЕХР(Х) нетрудно реализо­вать новую функцию с именем, например, POWER, осуществляющую возведение лю­бого вещественного числа в любую вещественную степень. В программе (пример 8.1) вводится пара чисел X и Y и выводится на экран дисплея результат возведения Х сна­чала в степень +Y, а затем - в степень -Y.

Для выхода из программы нужно ввести Ctrl-Z и Enter.

Пример 8.1

var

х,у: Real;

{------------------------}

Function Power(a,b ; Real): Real;

begin {Power}

if a > 0 then

Power := exp(b * ln(a))

else if a < 0 then

Power := exp(b * ln(abs(a)))

else if b = 0 then

Power := 1

else

Power := 0

end {Power};

{--------------------------}

begin {main}

repeat

readin(x,y) ;

writein(Power(x,y):12:10, Power(x,-y):15:10)

until EOF

end {main}.

Для вызова функции POWER мы просто указали ее в качестве параметра при обращении к встроенной процедуре WRITELN.

Параметры x и y в момент обращения к функции - это фактические параметры.

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

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

В программе функция POWER вызывается дважды - сначала с пара­метрами Х и Y, а затем X и -Y, поэтому будут получены два разных результата.

Механизм замены формальных параметров на фактические позволяет нужным об­разом настроить алгоритм, реализованный в подпрограмме.

Турбо Паскаль следит за тем, чтобы количество и тип формальных параметров строго соответствовали количе­ству и типам фактических параметров в момент обращения к подпрограмме.

Смысл используемых фактических параметров зависит от того, в каком порядке они перечис­лены при вызове подпрограммы.

В примере 8.1 первый по порядку фактический пара­метр будет возводиться в степень, задаваемую вторым параметром, а не наоборот.

Пользователь должен сам следить за правильным порядком перечисления фактиче­ских параметров при обращении к подпрограмме.

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

В предыдущем примере параметры А и В определены как параметры-значения.

Если па­раметры определяются как параметры-переменные, перед ними необходимо ставить заре­зервированное словоVAR, а если это параметры-константы,- словоCONST, например:

Procedure MyProcedure(var a: Real; b: Real; const c: String);

Здесь А - параметр-переменная, В -параметр-значение, а С - параметр-константа.

Определение формального параметра тем или иным способом существенно, в ос­новном, только для вызывающей программы:

если формальный параметр объявлен как параметр-переменная, то при вызове подпрограммы ему должен соответствовать фактический параметр в виде переменной нужного типа;

если формальный параметр объявлен как параметр-значение или параметр-константа, то при вызове ему может соответствовать произвольное выражение.

Контроль за неукоснительным соблюдени­ем этого правила осуществляется компилятором Турбо Паскаля.

Если бы для преды­дущего примера был использован такой заголовок функции:

Function Power (var a, b : Real) : Real;

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

Для того чтобы понять, в каких случаях использовать тот или иной тип парамет­ров, рассмотрим, как осуществляется замена формальных параметров на фактические в момент обращения к подпрограмме.

Если параметр определен как параметр-значение, то перед вызовом подпрограммы это значение вычисляется, полученный результат копируется во временную память и передается подпрограмме.

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

Любые возмож­ные, изменения в подпрограмме параметра-значения никак не воспринимаются вызы­вающей программой, так как в этом случае изменяется копия фактического параметра.

Если параметр определен как параметр-переменная, то при вызове подпрограммы передается сама переменная, а не ее копия (фактически в этом случае подпрограмме передается адрес переменной).

Изменение параметра-переменной приводит к измене­нию самого фактического параметра в вызывающей программе.

В случае параметра-константы в подпрограмму также передается адрес области памяти, в которой располагается переменная или вычисленное значение. Однако ком­пилятор блокирует любые присваивания параметру-константе нового значения в теле подпрограммы.

Представленный ниже пример 8.2 поясняет изложенное. В программе задаются два целых числа 5 и 7, эти числа передаются процедуре INC2, в которой они удваиваются.

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

Пример 8.2

const

а : Integer = 5;

b : Integer =7;

{--------------------}

Procedure Inc2 (var c: Integer; b: Integer);

begin {Inc2}

с := с + c;

b := b + b;

WriteLn ('удвоенные :',c:5,b:5)

end {inc2};

{---------------------}

begin {main}

WriteLn ('исходные :', a:5, b:5);

WriteLn('результат :', a:5, b:5)

end {main}.


В результате прогона программы будет выведено:

исходные : 5 7

удвоенные : 10 14

результат : 10 7


Как видно из примера, удвоение второго формального параметра в процедуре INC2 не вызвало изменения фактической переменной В, так как этот параметр описан в заголовке процедуры как параметр-значение.

Этот пример может служить еще и ил­люстрацией механизма «накрывания» глобальной переменной одноименной локаль­ной: хотя переменная В объявлена как глобальная (она описана в вызывающей про­грамме перед описанием процедуры), в теле процедуры ее «закрыла» локальная пере­менная В, объявленная как параметр-значение.

Итак, параметры-переменные используются как средство связи алгоритма, реали­зованного в подпрограмме, с внешним миром: с помощью этих параметров подпро­грамма может передавать результаты своей работы вызывающей программе.

Разуме­ется, в распоряжении программиста всегда есть и другой способ передачи результатов - через глобальные переменные.

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

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

С другой стороны, описание всех формальных параметров как параметров-переменных нежелательно по двум причинам.

Во-первых, это исключает возможность вызова подпрограммы с фактическими параметрами в виде выражений, что делает программу менее компактной.

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

Вот почему параметрами-переменными следует объявлять только те, через которые подпрограмма в действительности передает ре­зультаты вызывающей программе.

Чем меньше параметров объявлено параметрами-переменными и чем меньше в подпрограмме используется глобальных переменных, тем меньше опасность получения непредусмотренных программистом побочных эф­фектов, связанных с вызовом подпрограммы, тем проще программа в понимании и отладке.

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

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

Существует еще одно обстоятельство, которое следует учитывать при выборе вида формальных параметров.

Как уже говорилось, при объявлении параметра-значения осуществляется копирование фактического параметра во временную память. Если этим параметром будет массив большой размерности, то существенные затраты вре­мени и памяти на копирование при многократных обращениях к подпрограмме можно минимизировать, объявив этот параметр параметром-константой.

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


8.3. ПАРАМЕТРЫ-МАССИВЫ И ПАРАМЕТРЫ-СТРОКИ

Может сложиться впечатление, что объявление переменных в списке формальных параметров подпрограммы ничем не отличается от объявления их в разделе описания переменных.

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

Поэтому нельзя, например, объявить следующую процедуру:

Procedure S (a: array [1..10] of Real);

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

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

Например:

аtype = array [1..10] of Real;

Procedure S (a: atype);

…….

Поскольку строка является фактически своеобразным массивом, ее передача в подпрограмму осуществляется аналогичным образом:

type

intype = String [15];

outype = String [30];

Function St (s : intype) : outype;

……..

Требование описать любой тип-массив или тип-строку перед объявлением подпро­граммы на первый взгляд кажется несущественным.

Действительно, в рамках про­стейших вычислительных задач обычно заранее известна структура всех используе­мых в программе данных, поэтому статическое описание массивов не вызывает про­блем.

Однако разработка программных средств универсального назначения связана со значительными трудностями.

По существу, речь идет о том, что в Турбо Паскале невозможно использовать в подпрограммах массивы с «плавающими» границами изменения индексов.

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

Этот недостаток, как и отсутствие в языке средств обработки исключительных ситуаций (прерываний), унаследован из стандартного Паскаля и представляет собой объект постоянной и вполне заслуженной его критики.

Разработчики Турбо Паскаля не риск­нули кардинально изменить свойства базового языка, но, тем не менее, включили в него некоторые средства, позволяющие в известной степени смягчить отмеченные недостатки.

Прежде всего, в среде Турбо Паскаля можно установить режим компиляции, при котором отключается контроль за совпадением длины фактического и формального параметра-строки (см. прил.1). Это позволяет легко решить вопрос о передаче подпро­грамме строки произвольной длины.

При передаче строки меньшего размера фор­мальный параметр будет иметь ту же длину, что и параметр обращения; передача строки большего размера приведет к ее усечению до максимального размера фор­мального параметра.

Следует сказать, что контроль включается только при передаче строки, объявленной как формальный параметр-переменная.

Если соответствующий параметр объявлен параметром-значением, эта опция игнорируется и длина не кон­тролируется.

Значительно сложнее обстоит дело с передачей массивов произвольной длины.

Наиболее универсальным приемом в этом случае будет, судя по всему, работа с указа­телями и использование индексной арифметики.

Несколько проще можно решить эту проблему при помощи нетипизированных параметров (см. п.8.5). В версии Турбо Пас­каля 7.0 язык поддерживает так называемые открытые массивы, легко решающие проблему передачи подпрограмме одномерных массивов переменной длины.

Открытый массив представляет собой формальный параметр подпрограммы, опи­сывающий базовый тип элементов массива, но не определяющий его размерности и границы:


Procedure MyProc(OpenArray: array of Integer);


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

{Иллюстрация использования открытых массивов:

программа выводит на экран содержимое двух одномерных массивов разной длины с помощью одной процедуры Array Print}

Procedure ArrayPrint(aArray: array of Integer);

var

k: Integer;

begin

for k := 0 to High(aArray) do Write(aArray[k]:8) ;

WriteLn

end;


(Эти недостатки практически полностью устранены в языке Object Pascal, используемом в визуальной среде программирования Delphi.)

const

A: array [-1..2] of Integer = (0,1,2,3);

В: array [5..7] of Integer = (4,5,6);

begin

ArrayPrint(A) ;

ArrayPrint(B)

end.


Как видно из этого примера, фактические границы массивов А и В, передаваемых в качестве параметров вызова процедуре ArrayPrint, не имеют значения.

Однако раз­мерность открытых массивов (количество индексов) всегда равна 1 - за этим следит компилятор.

Если бы, например, мы добавили в программу двумерный массив С


var

С: array [1..3,1..5] of Integer;


то обращение


ArrayPrint(С)


вызвало бы сообщение об ошибке


Error26: Type mismatch.

(Ошибка 26: Несоответствие типов.)


8.4. ПРОЦЕДУРНЫЕ ТИПЫ. ПАРАМЕТРЫ-ФУНКЦИИ И ПАРАМЕТРЫ-ПРОЦЕДУРЫ

Процедурные типы - это нововведение фирмы Borland (в стандартном Паскале та­ких типов нет).

Основное назначение этих типов - дать программисту гибкие средства передачи функций и процедур в качестве фактических параметров обращения к дру­гим процедурам и функциям.

Для объявления процедурного типа используется заголовок процедуры (функции), в котором опускается ее имя, например:


type

Proc1 = Procedure (a, b, c: Real; var d: Real) ;

Proc2 = Procedure (var a, b);

РгосЗ = Procedure;

Func1 = Function: String;

Func2 = Function (var s: String): Real;


Как видно из приведенных примеров, существует два процедурных типа: тип-процедура и тип-функция.

Пример 8.3 иллюстрирует механизм передачи процедур в качестве фактических параметров вызова. Программа выводит на экран таблицу двух функций:


sinl(x) = (sin(x) +1) * ехр(-х)


и


cosl (х) = (cos(x) +1) * ехр(-х).


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

Пример 8.3.

Uses CRT;

type

Func = Function (x: Real): Real;

{------------------------}

Procedure PrintFunc(XPos: Byte; F:Func);

{Осуществляет печать функции F (XPos - горизонтальная

позиция начала вывода)}

const

np = 20; {Количество вычислений функций}

var

x:real;

I:integer;


begin {PrintFunc}

for i : = 1 to np do

begin

x := i * (2 * pi / np) ;

GotoXY (XPos, WhereY);

WriteLn (x:5:3, F(x):18:5)

End

end; {PrintFunc}

{------------------}

Function Sinl(x: Real): Real; far;

Begin

sinl := (sin(x) + 1) * exp(-x)

end;

{---------------------}


Function Cosl(x: Real): Real; far;

begin

cosl := (cos(x) + 1) * exp(-x)

end;

{----------------------}

begin {основная программа)

CIrScr; {Очищаем экран}

PrintFunc (1, sin1);

GotoXY (1,1); {Переводим курсор

в левый верхний угол}

PrintFunc (40, cos1)

end.


Обратите внимание: для установления правильных связей функций SIN1 и COS1 с Процедурой PRINTFUNC они должны компилироваться с расчетом на дальнюю мо­дель памяти. Вот почему в программу вставлены стандартные директивыFAR сразу за заголовками функций.

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

Стандартные процедуры (функции) Турбо Паскаля не могут передаваться рассмотренным способом.

В программе могут быть объявлены переменные процедурных типов, например, так:

var

p1 : Proс1;

fl, f2 : Func2;

ар : array [1..N] of Proс1;


Переменным процедурных типов допускается присваивать в качестве значений имена соответствующих подпрограмм.

После такого присваивания имя переменной становится синонимом имени подпрограммы, например:

type

Proc = Procedure (n: word; var a: Byte) ;

var

ProcVar: Proc;

x, у : Byte;

Procedure Proc1(x: word; var y: Byte); far;

begin

if x > 255 then

y:=x mod 255

else

у := Byte(x)

end;

begin {Главная.программа}

ProcVar := Proc1;

for x := 150 to 180 do

begin

ProcVar (x + 100, у);

Write (у:8)

end

end.


Разумеется, такого рода присваивания допустимы и для параметров-функций, пример:

type

FuncType = Function (i : Integer) : Integer;

vаг

VarFunc : FuncType;

i : Integer;

Function MyFunc (count : Integer) : Integer; far;

Begin

....

end; {MyFunc}

begin {Основная программа}

..

i :=MyFunc(l); (Обычное использование результата функции}

…..

VarFunc := MyFunc; {Присваивание переменной процедурного типа имени функции MyFunc}

……

end.


Отметим, что присваивание

VarFunc := MyFunc(l);


будет недопустимым, так как слева и справа от знака присваивания используются несовместимые типы: слева - процедурный тип, а справа - INTEGER; имя функции со списком фактических параметров MyFunc(1) трактуется Турбо Паскалем как обраще­ние к значению функции, в то время как имя функции без списка параметров рассмат­ривается как имя функции.

В отличие от стандартного Паскаля, в Турбо Паскале разрешается использовать в передаваемой процедуре (функции) любые типы параметров: параметры-значения, параметры-переменные, параметры-константы (в стандартном Паскале только пара­метры-значения).


8.5. НЕТИПИЗИРОВАННЫЕ ПАРАМЕТРЫ-ПЕРЕМЕННЫЕ ,,

Еще одно очень полезное нововведение фирмы Borland - возможность использова­ния нетипизированных параметров.

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

Нетипизированные параметры обычно используются в случае, когда тип данных несущественен.

Такие ситуации чаще всего возникают при разного рода копированиях одной области памяти в другую, например, с помощью процедур BLOCKREAD, BLOCKWRITE, MOVE и т.п.

Нетипизированные параметры в сочетании с механизмом совмещения данных в памяти (см. п.4.4) можно использовать для передачи подпрограмме одномерных мас­сивов переменной длины (этот способ можно использовать в Турбо Паскале версии 6.0 и более ранней, в которых нет открытых массивов).

В примере 8.4 функция NORMA вычисляет норму вектора, длина которого меняет­ся случайным образом. Стандартная константа MAXINT содержит максимальное зна­чение целого типа INTEGER и равна 32767.

Следует учесть, что при обращении к функции NORMA массив Х помещается в стек и передается по ссылке, поэтому описание локальной переменной А в виде одно­мерного массива максимально возможной длины в 65532 байта (встроенная константа MAXINT определяет максимально возможное значение типа INTEGER и равна 32767), совпадающего с X, на самом деле не приведет к выделению дополнительного объема памяти под размещение этой переменной.

Иными словами, переменная А - фиктивная переменная, размер которой никак не влияет на объем используемой памяти, С таким же успехом можно было бы объявить ее в виде массива из одного элемента, правда, в этом случае необходимо позаботиться об отключении контроля выхода индекса за границы диапазона.

Пример 8.4

const

NN = 100; {Максимальная длина вектора}

var

а : array [1..NN] of Real;

i, j, N : Integer;

{------------------}

Function Norma (var x; N: Integer): Real;

var

a : array [1..2*MaxInt div SizeOf(Real)] of Real absolute x;

i : Integer;

s : Real;

begin {Norma}

s :=0;

for i :=1 to N do

s := s + sqr(a[i]) ;

Norma := sqrt(s)

end {Norma};

{------------------}

begin {main}

for i := 1 to 10 do

begin

N := Random(NN) + 1; {Текущая длина вектора}

for j := 1 to N do

a[j] := Random;

WriteLn ('N = ', N:2, ' норма =', Norma (a, N) : 10:7)

end

end {main}.


Как видно из рассмотренного примера, передача одномерных массивов переменной длины не вызывает никаких трудностей.

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

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


8.6. РЕКУРСИЯ И ОПЕРЕЖАЮЩЕЕ ОПИСАНИЕ

Рекурсия - это такой способ организации вычислительного процесса при котором подпрограмма в ходе выполнения составляющих ее операторов обращается сама к себе.

Рассмотрим классический пример - вычисление факториала (пример 18)). Программа вводит с клавиатуры целое число N и выводит на экран значение –N!, которое вычисляется с помощью рекурсивной функции FAC.

Для выхода из программы необходимо либо ввести достаточно большое целое число, чтобы вызвать nepеполнение при умножении чисел с плавающей запятой, либо нажать Ctrl-Z и Enter.

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

В примере 8.5 решение при N =0 триви­ально и используется для остановки рекурсии.

Пример 8.5

Program Factorial;

{$S+} {Включаем контроль переполнения стека}

var

n: Integer;

Function Fac(n: Integer): Real;

{Рекурсивная функция, вычисляющая n!}

begin {Fac}

if n < 0 then

WriteLn ('Ошибка в задании N')

else

if n=0 then

Fac := 1

else Fac := n * Fac(n-l)

end {Fac};

{-----------------}

begin {main}

repeat

ReadLn(n) ;

WriteLn('n! = ',Fac(n))

until EOF

end {main}.


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

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

Если, например, попытаться заменить тип REAL функции FAC (см. пример 8.5) на EXTENDED, программа перестанет работать уже при N = 8.

Чтобы избежать пере­полнения стека сопроцессора, следует размещать промежуточные результаты во вспомога­тельной переменной.

Вот правильный вариант примера 8.5 для работы с типом EXTENDED:


Program Factorial;

{$S+,N+,E+} {Включаем контроль стека и работу сопроцессора}

var

n: Integer;

Function Fac(n: Integer): extended;

var

F: extended; {Буферная переменная для разгрузки стека сопроцессора}

{Рекурсивная функция, вычисляющая п!}

begin {Fac}

if n < 0 then

WriteLn ('Ошибка в задании N')

else

if n = 0 then

Fac := 1

Else

Begin

F := Fac(n-l);

Fac := F * n

End

end {Fac};

{------------------}

begin {main}

repeat

ReadLn(n) ;

WriteLn('n! = ',Fac(n))

until EOF

end {main}.


Рекурсивный вызов может быть косвенным. В этом случае подпрограмма обраща­ется к себе опосредованно, путем вызова другой подпрограммы, в которой содержится обращение к первой, например:

Procedure A (i : Byte) ;

Begin

……

B(i);

Procedure В (j : Byte) ;

…..

begin

……

A(j);

……

end;


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

Для того, чтобы такого рода вызовы стали возможны, вводится опере­жающее описание:


Procedure В (j : Byte); forward;

Procedure A (i : Byte) ;

Begin

…..

В (i);

…..

end;

Procedure В;

Begin

…..

A(j);

..

end;


Как видим, опережающее описание заключается в том, что объявляется лишь заго­ловок процедуры В, а ее тело заменяется стандартной директивойFORWARD.

Теперь в процедуре А можно использовать обращение к процедуре В - ведь она уже описана, точнее, известны ее формальные параметры, и компилятор может правильным обра­зом организовать ее вызов. Обратите внимание: тело процедуры В начинается заго­ловком, в котором уже не указываются описанные ранее формальные параметры.


8.7. РАСШИРЕННЫЙ СИНТАКСИС ВЫЗОВА ФУНКЦИЙ


В Турбо Паскале есть возможность вызывать функцию и не использовать то значе­ние, которое она возвращает.

Иными словами, вызов функции может внешне выгля­деть как вызов процедуры, например:

{$Х+} {Включаем расширенный синтаксис}

Function MyFunc(var x : Integer) : Integer;

begin

if x<0 then x:=0

else MyFunc := x+10

end; {MyFunc}

var

i : Integer;

begin {main}

i := 1;

i := 2*MyFunc(i)-100; {Стандартный вызов функции}

MyFunc(i) {Расширенный синтаксис вызова}

end. {main}


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

С помощью расширенного синтаксиса нельзя вызывать стандартные функ­ции. Компиляция с учетом расширенного синтаксиса включается активным состояни­ем опции EXTENDED SYNTAX диалогового окна OPTIONS/COMPILER (см. прил.1) или глобальной директивой компилятора {$Х+}.