Pers.narod.ru. Обучение. Учебник по Паскалю. Глава 18

18. Подпрограммы

 

Написание любой большой программы невозможно как без разбиения задачи на менее сложные подзадачи, которые мы можем решать независимо, так и без повторного использования ранее написанного кода (представим, что каждая новая программа писалась бы "с нуля"?!). Решить эти важнейшие задачи позволяет механизм подпрограмм, имеющийся в любом языке программирования, в том числе, и в Паскале.

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

Использование подпрограмм в чем-то похоже на расчеты с использованием математических или физических формул. Так, имея общие формулы решения квадратного уравнения, мы можем подставить вместо коэффициентов a, b и c любые числовые значения и решить любое конкретное уравнение. Аналогично, мы могли бы написать подпрограмму с входными параметрами a, b, c и выходными параметрами x1, x2 (найденные корни уравнения), а затем, применяя нужное число раз эту подпрограмму (однократное использование подпрограммы называется ее вызовом), найти корни любого количества квадратных уравнений.

Итак, использование подпрограмм позволяет решить следующие задачи:

·       уменьшение размеров кода и экономия памяти за счет возможности неоднократного вызова одной и той же подпрограммы в рамках одной программы;

·       лучшее структурирование программы за счет разбиения задачи на более простые подзадачи;

·       эффективное повторное использование однажды написанного кода.

Рассмотрим общую структуру сложной программы, содержащей  две подпрограммы:

var раздел_описаний_1;

 

Заголовок Подпрограммы_1;

begin

 {Тело подпрограммы_1}

end;

 

Заголовок Подпрограммы_2;

begin

 {Тело подпрограммы_2}

end;

 

var раздел_описаний_2;

begin

 {Тело главной программы}

end.

Как видно из листинга, каждая подпрограмма имеет заголовок (по меньшей мере, в этом заголовке должно быть указано ее назначенное программистом имя) и тело, состоящее из операторов. Подобно телу цикла, тело подпрограммы заключено в операторные скобки begin ... end;. Обратите внимание, что в листинге два раздела описаний. Первый из них  расположен до обеих подпрограмм, второй -- после них перед телом главной программы. Данные, описанные в первом разделе -- глобальные, они доступны всем частям программы, расположенным ниже по ее тексту. Данные второго раздела описаний также глобальны, но доступны лишь главной программе, так как описаны непосредственно перед ней. Общее правило очень простое: подпрограммы "видят" все глобальные переменные, описанные выше их тела. Аналогично, без принятия специальных мер подпрограмма "видит" и может вызвать любую другую подпрограмму, расположенную выше нее по тексту программы. Здесь вторая подпрограмма  может вызвать первую, но не наоборот. Главная программа, как правило, расположенная последней, может вызвать все подпрограммы.

На практике не рекомендуется делать подпрограммы зависящими от глобальных данных, так как это снижает их переносимость (возможность повторного использования). Разумеется, любого из разделов описаний в конкретной программе может и не быть. Более того, поскольку подпрограмма -- отдельная и, в идеале, независимая часть программы, она может содержать собственный раздел описания локальных переменных, предназначенных лишь для ее нужд и невидимых из других частей программы. Например, для подпрограммы решения квадратного уравнения такой локальной переменной могла бы быть переменная d, предназначенная для вычисления дискриминанта уравнения. Поясним этот важный момент на примере:

var i:integer;

    {глобальная переменная -

     описана вне всех подпрограмм}

Заголовок Подпрограммы;

var i:integer;

    {локальная переменная -

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

begin

 {Тело подпрограммы}

end;

 

begin

{Тело главной программы}

end.

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

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

 

18.1. Процедуры

 

Общий вид подпрограммы-процедуры следующий:

procedure Имя

           (Список формальных параметров);

{выше - заголовок подпрограммы}

var описания локальных переменных;

begin

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

end;

Другие подразделы раздела описаний, такие как label, const и изучаемый в п. 18.3 оператор type, также могут присутствовать между заголовком и телом процедуры и действие их также будет локально -- то есть, определено лишь в рамках данной процедуры. Единственная правильная связь процедуры с "внешним миром", то есть, другими подпрограммами и главной программой -- указанный после имени список формальных параметров. В этом списке через запятую указываются входные и выходные параметры процедуры с указанием их типов данных. Входные параметры служат исходными данными для процедуры, а выходные определяют результаты ее вычислений, передаваемые в главную программу или другую подпрограмму. Перед выходными параметрами, измененные значения которых должны сохраняться после завершения процедуры, следует указывать ключевое слово var. Причины именно такого указания мы обсудим ниже.

Само по себе написание процедуры еще не вызывает выполнения никаких действий. Чтобы процедура сработала, ее нужно вызвать, записав в нужной точке программы имя процедуры со списком фактических параметров, которые будут подставлены на место формальных. Все это делается не так сложно, как звучит. Представим, что мы решили написать процедуру решения любого квадратного уравнения с именем Equation. Входными данными этой процедуры будут значения коэффициентов уравнения a, b и c, выходными -- найденные корни x1 и x2 (если они существуют). Кроме того, нам понадобится "временная" (а точней, локальная) переменная d, позволяющая процедуре хранить вычисленное значение дискриминанта:

procedure Equation (a,b,c:real;

                    var x1,x2:real);

var d:real;

begin

 d:=sqr(b)-4*a*c;

 if d>=0 then begin

  x1:=(-b+sqrt(d))/(2*a);

  x2:=(-b-sqrt(d))/(2*a);

 end;

end;

Поскольку все параметры этой процедуры -- вещественные, в ее заголовке нам хватило двух списков -- один для входных параметров a, b и c, другой -- для выходных x1 и x2 (обратите внимание на слово var перед этим списком). В случае отрицательного значения дискриминанта, значения x1 и x2 остаются неопределенными (что, вообще говоря, не совсем корректно), в противном случае они вычисляются и должны быть переданы в главную программу. Вызвать эту процедуру мы могли бы, например, так:

var a,b,c,d,x1,x2,x3,x4:real;

...

write ('Введите значения a,b,c:');

read (a,b,c);

Equation (a,b,c,x1,x2);

Здесь мы попытались решить уравнение ax2+bx+c=0, значения a, b, c, ввел пользователь, ответы, если они вычислялись, будут помещены в переменные x1 и x2. При вызове вида

Equation (1,4,-3.5,x3,x4);

мы решили уравнение x2+4x-3.5=0, а ответы поместили в переменные x3 и x4. Еще один вызов

Equation (4,1,-3.5,x3,x4);

позволяет решить уравнение 4x2+x-3.5=0, записав ответы в переменные x1 и x2. Наконец, четвертое обращение к процедуре

Equation (1,b,0,x1,x3);

решает уравнение x2+bx=0, помещая ответы в переменные x1 и x3.

И так далее. Суть в том, что при каждом вызове подпрограммы значения фактических параметров подставляются на место формальных и с ними производятся вычисления, предусмотренные операторами подпрограммы. Указанные требования называют согласованием параметров и описывают следующим образом: формальные и фактические параметры должны быть согласованы между собой по количеству, типу и порядку следования. Это означает, что количество формальных и фактических параметров должно быть одинаковым, при этом, при вызове процедуры каждый фактический параметр должен иметь тот же тип и занимать в списке то же место, что и соответствующий ему формальный параметр. Из сказанного следует, что нашу процедуру Equation можно вызвать только с пятью параметрами (а не тремя, семью или нулём), причем все эти параметры должны быть вещественными. Если формальный параметр является выходным (перед ним в заголовке процедуры указано ключевое слово var), то соответствующий фактический параметр не может быть константой (ведь значение константы нельзя изменить). Для больше наглядности опишем данное соответствие в табл. 18.1.

 

Табл. 18.1. Соответствие формальных и фактических параметров

Формальный параметр в заголовке процедуры

Соответствующий фактический параметр при вызове процедуры

Переменная некоторого типа данных без ключевого слова var

Переменная, константа, элемент массива или значение выражения того же типа данных

Переменная некоторого типа данных с ключевым словом var

Переменная или элемент массива того же типа данных

Массив

Массив

 

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

procedure p1 (x:integer);

      {для x создастся локальная копия}

begin

 x:=x+1; {значение копии увеличилось на 1}

 writeln ('x=',x); {и было выведено}

end;

 

var x:integer;

begin

 x:=3;

 p1(x);

 writeln ('x=',x);

 {после вызова с передачей параметра

  по значению, x по-прежнему равно 3}

end.

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

procedure p1 (var x:integer);

 {получаем адрес переменной,

  переданной в качестве x}

begin

 x:=x+1;   {значение x увеличилось на 1}

 writeln ('x=',x);     {и было выведено}

end;

 

var x:integer;

begin

 x:=3;

 p1(x);

 writeln ('x=',x);

 {после вызова с передачей параметра

  по ссылке,x стало равно 4 -

  оно было изменено процедурой p1}

end.

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

Перейдем к примерам.

1. Для точки на плоскости с заданными координатами (x,y) найти расстояние l от точки до начала координат, а также длину окружности и площадь круга радиусом l с центром в начале координат.

Обозначим длину окружности как c, а площадь круга -- s. Нетрудно заметить, что помимо выделения в отдельную процедуру расчета величин l, c и s, целесообразно написать также процедуру Get для ввода вещественного значения с предварительным выводом приглашения к вводу (нам придется вводить, по меньшей мере, 2 значения -- x и y), а также процедуру Put для печати вещественного значения с указанной шириной, точностью и строкой комментария (выводиться будут искомые величины l, c и s).

procedure Circle2 (x,y:real;

 var l,c,s:real);

const pi=3.1415926;

 {для иллюстрации; на самом деле

  есть функция pi}

begin

 l:=sqrt(sqr(x)+sqr(y));

 c:=2*pi*l;

 s:=pi*sqr(l);

end;

 

procedure Get (s:string; var x:real);

 {s - приглашение,

  x - параметр-переменная,

      вводимая с клавиатуры величина}

begin

 write (s,': ');

 readln (x);

end;

 

procedure Put (s:string; x:real);

 {s - комментарий к выводу,

  x - выводимая величина}

begin

  writeln (s,'= ',x:8:3);

end;

 

var x,y,c,s,l:real;

 

begin

  writeln;

  Get ('Введите координату x',x);

  Get ('Введите координату y',y);

  Circle2 (x,y,l,c,s);

  Put ('Удаление от начала координат',l);

  Put ('Длина окружности',c);

  Put ('Площадь круга',s);

end.

Несмотря на то, что процедура Circle2 вызвана в этой программе однократно, она может пригодиться при повторном использовании кода. Исходными данными (параметрами-значениями) для этой процедуры являются координаты точки x и y, выходными данными (параметры-переменные) -- найденные значения l, c и s.

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

2. Написать процедуру, печатающую первые N членов ряда Фибоначчи.

Члены ряда Фибоначчи вычисляются по формуле:

 F0  = 0,  F1  = 1,  FN  = FN-1   + FN-2,  N=2,3,...

Заметим, что кроме простой и красивой формулы, ряд Фибоначчи замечателен еще тем, что отношение двух его соседних членов дает в пределе константу "золотого сечения", широко используемую в самых различных расчетах. Для вычисления и вывода на экран N первых членов ряда Фибоначчи напишем процедуру Fibo с формальным параметром all, определяющим, сколько членов ряда требуется найти. В главной программе организуем цикл ввода значения фактического параметра N.

procedure Fibo (all:integer);

var n,n1,n2:longint;

{n - текущий элемент ряда,

 n1 - предыдущий,

 n2 - вычисленный 2 шага назад}

    i:integer;

begin

 n1:=1; n2:=0;

 writeln;

 write (n2,' ',n1,' ');

 for i:=2 to all do begin

  n:=n1+n2;

  write (n,' ');

  n2:=n1; n1:=n;

    {переприсвоили для следующего шага}

 end;

end;

 

var n:integer;

begin

 repeat

  writeln ('Введите число членов ряда ',

           'или 0 для выхода:');

  read (n);

  if n=0 then halt(1)

  else Fibo(n);

 until false;

end.

 

18.2. Функции

 

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

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

function Имя

 (Список формальных параметров) :

   ТипРезультата;

{выше - заголовок подпрограммы}

var описания локальных переменных;

begin

  {Тело функции}

   Имя:=Выражение1;

end;

Здесь Выражение1 должно иметь тот же тип, что и указанный в заголовке ТипРезультата, а оператор Имя:=Выражение1; не обязан быть последним в теле функции. Поскольку результат выполнения функции возвращается в основную программу через ее имя, то обращение к функции записывается аналогично стандартным функциям в виде операнда-выражения, стоящего справа от знака присваивания:

Результат:=Выражение2;

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

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

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

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

Чтобы окончательно уяснить не-синтаксические различия между функциями и процедурами, обобщим их в табл. 18.2.

 

Табл. 18.2. Различия между процедурами и функциями

Процедура

Функция

Вызывается отдельным оператором

Вызывается из выражения справа от знака присваивания

Использует параметры-значения и переменные

Использует параметры-значения и переменные, дополнительно доступен параметр-переменная с именем, совпадающим с именем функции

 

Напишем и вызовем простейшую функцию, находящую максимальный из двух своих вещественных аргументов:

function max(a,b:real):real;

begin

 if a>b then max:=a

 else max:=b;

end;

Вызвать эту функцию мы могли бы различными способами:

var x,y,z,r,t:real;

...

read (x,y,z);

r:=max(x,y);

С помощью функции вычислили максимальное из значений x, y и записали его в переменную r.

t:=max(max(x,y),z);

Максимальное из значений x, y, z записали в переменную t.

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

procedure max (a,b:real; var c:real);

begin

 if a>b then c:=a

 else c:=b;

end;

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

Мы уже знакомы с оператором halt;, позволяющим аварийно (или просто из бесконечного цикла) завершить программу. Немедленно завершить текущий блок, в том числе и подпрограмму-функцию или процедуру, позволяет оператор Exit;.

Перейдем к примерам.

1. Используя подпрограмму, вычислить сумму первых k членов ряда 1+1/n.

Сумма ряда -- это скаляр, естественным выглядит использование подпрограммы-функции. Применив известные алгоритмы, составим программу:

function sum (k:integer):real;

var i:integer; s:real;

begin

 s:=0;

 for i:=1 to k do s:=s+1/i;

 sum:=s;

end;

 

var k:integer; s:real;

begin

 write ('Введите число шагов:');

 readln (k);

 s:=sum(k);

 writeln ('Сумма=',s:8:3);

end.

Обратите внимание -- несмотря на то, что функция sum вычисляет единственную величину s, мы были бы не вправе написать в ней оператор

for i:=1 to k do sum:=sum+1/i;,

поскольку sum -- это имя функции, и справа от знака присваивания оно было бы воспринято как попытка функции sum вызвать саму себя, причем, без соблюдения правил соответствия параметров. Поэтому нам понадобилась локальная переменная s.

Тем не менее, рекурсивные функции, вызывающие сами себя, существуют и будут кратко рассмотрены далее.

2. Вычислить значение выражения , где z(x)= sin 2x + ln |x|.

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

function z(x:real):real;

begin

 z:=sin(2*x)+ln(abs(x));

end;

 

begin

 write ('Ответ=', (z(3)+2*sqr(z(5)))/

                  (z(0.1)+1):6:2);

 readln;

end.

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

3. В заключение раздела скажем несколько слов о рекурсии. Рекурсивными называют функции, способные повторно вызывать сами себя. С точки зрения программирования, в этом нет ничего удивительного -- просто при повторном входе в функцию в программном стеке создается ее новая копия и расчет выполняется заново с измененным значением параметра функции. Затем функция проверяет значение параметра, при необходимости изменяет его и вновь повторно вызывает сама себя. Разумеется, во избежание зацикливания, при некотором значении параметра должно быть предусмотрено завершение вычислительного процесса. Использование рекурсии целесообразно везде, где расчет следующего значения некоторой функции зависит от ее предыдущего значения. Так, классический пример на рекурсию -- расчет факториала (факториал целого положительного числа n, обозначаемый n!, равен произведению всех чисел от 1 до N включительно). Используя очевидную формулу n!=n*(n-1)!, напишем следующую рекурсивную функцию:

function Factorial (n:integer):longint;

begin

 if n<2 then Factorial:=1

 else Factorial:=n*Factorial(n-1);

end;

Тип функции longint использован, так как факториал очень быстро растет и уже 8!=40320, то есть, больше максимального значения типа integer. Эта функция, как нетрудно увидеть, при значении аргумента, меньшем двух, возвращает единицу, а в противном случае возвращает свой аргумент n, умноженный на факториал от n-1. Несложный тест функции мог бы выглядеть так:

begin

 writeln ('10!=',Factorial(10));

end.

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

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

 

18.3. Массивы в качестве параметров подпрограммы

 

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

type ИмяТипа = ОписаниеМассива;

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

type vector = array [1..10] of real;

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

В дальнейшем тип данных vector можно использовать как в разделе описания переменных

var a,b:vector;,

так и в списке формальных параметров функции или процедуры:

function work (a:vector):real;

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

Допустим, с помощью подпрограммы требуется найти максимальные элементы матриц A3x4, B4x3, C4x5. С учетом максимальной из имеющихся размерностей, делаем объявление типа matrix в разделе описаний:

type matrix = array [1..4,1..5] of real;

Далее описываем конкретные матрицы как объекты нового типа данных matrix:

var a,b,c:matrix;

В качестве подпрограммы поиска значения максимального элемента матрицы напишем функцию max1 (функцию, а не процедуру, поскольку максимум -- скалярное значение). В эту функцию нам придется передать 2 дополнительных параметра-значения, определяющих количество строк n и количество столбцов m в конкретной матрице, переданной фактическим параметром:

function max1 (n,m:integer;

               var a:matrix):real;

var max:real; i,j:integer;

begin

 max:=a[1,1];

 for i:=1 to n do

 for j:=1 to m do

  if a[i,j]>max then max:=a[i,j];

 max1:=max;

end;

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

Вызвать нашу функцию для матриц A, B и C мы могли бы так:

var ma,mb,mc:real;

...

ma:=max1(3,4,a);

mb:=max1(4,3,b);

mc:=max1(4,5,c);

Более удобную работу с массивами-параметрами предлагает механизм открытых массивов, рассмотренный в п. 18.4. Однако его непосредственно используют лишь для одномерных массивов.

Пока же рассмотрим примеры решения типовых задач.

1. Заданы вектора A и B, содержащие по 5 элементов. Используя подпрограмму, найти их скалярное произведение по формуле

Поиск скалярного произведения реализуем в виде подпрограммы-функции scal.

type vector=array [1..5] of real;

 

function scal (n:integer;

               var a,b:vector):real;

var i:integer;

    s:real;

begin

 s:=0;

 for i:=1 to n do s:=s+a[i]*b[i];

 scal:=s;

end;

 

var i:integer;

    a,b:vector;

    s:real;

begin

 writeln ('Вектор 1 из 5 элементов:');

 for i:=1 to 5 do read (a[i]);

 writeln ('Вектор 2 из 5 элементов:');

 for i:=1 to 5 do read (b[i]);

 s:=scal(5,a,b);

 writeln ('s=',s:10:3);

end.

2. Сформировать по введенному с клавиатуры вектору A размерности n вектор res, компонентами которого являются отклонения элементов A от их арифметического среднего (подобная задача уже решалась выше, расширим ее на случай вектора).

Задача предполагает написание, по меньшей мере, двух подпрограмм: функция Middle будет вычислять арифметическое среднее элементов вектора, а процедура Otkl - формировать по вектору A и ранее найденному среднему mid искомый вектор отклонений b. Компоненты вектора b при этом будут вычисляться по формуле . Поскольку о размерности векторов в задаче ничего не сказано, укажем в разделе type максимальную размерность, равную 100 элементам.

type vector= array [1..100] of real;

 

function Middle (n:integer;

                 var a:vector):real;

var j:integer;

    res:real;

begin

 res:=0.0;

 for j:=1 to n do res:=res+a[j];

 Middle:=res/n;

end;

 

procedure Otkl (n:integer; mid:real;

                var a,b:vector);

var j:integer;

begin

 for j:=1 to n do b[j]:=abs(a[j]-mid);

end;

 

var a,res: vector;

    i,n:integer;

    s:real;

begin

 write ('Размерность? ');

 readln (n);

 for i:=1 to n do begin

  write ('A[',i,']=');

  readln (a[i]);

 end;

 s:=Middle (n,a);

 Otkl (n,s,a,res);

 for i:=1 to n do

  writeln ('res[',i,']=',res[i]:8:2);

end.

3. Используя подпрограмму, написать и проверить программу перемножения двух матриц.

Как известно, матрица A размерностью n3m может быть умножена на матрицу B размерностью m3p по следующей формуле: где ci,j -- элемент получающейся в результате перемножения матрицы С размерностью n3m. Из формулы видно, что для умножения двух матриц нам понадобится тройной цикл: внешний цикл по i перебирает строки матрицы A, вложенный в него цикл по j выбирает в матрице B очередной столбец, а самый внутренний цикл по l умножает строку матрицы A на столбец матрицы B, получая элемент ci,j. Напишем соответствующую процедуру mmul и тестовую программу для нее:

type matrix=array[1..10,1..10] of real;

 

var a,b,c: matrix;

    i,j,n,m,p: integer;

 

procedure mmul (n,m,k:integer;

                var a,b,c:matrix);

var i,j,l:integer; s:real;

begin

 for i:=1 to n do

 for j:=1 to k do begin

     s:=0;

     for l:=1 to m do s:=s+a[i,l]*b[l,j];

     c[i,j]:=s;

 end;

end;

 

begin

 repeat

  writeln;

  write ('Введите количество строк ',

         '1 матрицы: ');

  readln (n);

  write ('Введите количество столбцов ',

         '1 матрицы: ');

  readln (m);

  write ('Введите количество столбцов ',

         '2 матрицы: ');

  readln (p);

 until (n>1) and (n<11) and (m>1)

        and (m<11) and (p>1) and (p<11);

 for i:=1 to n do begin

  writeln ('Введите строку ',i,

   ' матрицы A из',m,'элементов:');

  for j:=1 to m do read (a[i,j]);

 end;

 for i:=1 to m do begin

  writeln ('Введите строку ',i,

   ' матрицы B из',p,'элементов:');

  for j:=1 to p do read (b[i,j]);

 end;

 

 mmul (n,m,p,a,b,c);

 for i:=1 to n do begin

  writeln;

  for j:=1 to p do write (c[i,j]:10:3);

 end;

end.

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

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

Имеется N городов, между которыми налажены пассажирские перевозки. Между какими городами самый активный пассажиропоток?

Количество городов обозначим константой cities. После математической формализации задачи, нетрудно заметить, что перевозки из города i в город j могут быть занесены в элемент матрицы ai,j, таким образом, требуется определить величину max(ai,j+aj,i), учитывая перевозки "туда" и "обратно". Для поиска максимального пассажиропотока достаточно двойного цикла с переменной границей по счетчику вложенного цикла. Как и в других программах, выделим в отдельные подпрограммы также типовые задачи ввода и вывода матрицы, а также ввода вещественного значения с контролем допустимых числовых границ ввода.

const cities=10;

type matrix=array [1..cities,1..cities]

 of integer;

 

function max1 (n:integer; var a:matrix;

 var imax,jmax:integer):integer;

var i,j,m,p:integer;

begin

 m:=a[1,2]; imax:=1; jmax:=2;

 for i:=1 to n do

 for j:=1 to n do

  if (i<>j) then begin

   p:=a[i,j]+a[j,i];

   if p>m then begin

    m:=p; imax:=i; jmax:=j;

   end;

  end;

 max1:=p;

end;

 

function readNumber (s:string;

 min,max:integer):integer;

var a:integer;

begin

 repeat

  write (s);

  {$I-}readln(a);{$I+}

  if IoResult<>0 then

   writeln ('Ошибка, введено не число!')

  else if (a<min) or (a>max) then

   writeln ('Ошибка, введенное число ',

   'принадлежит интервалу [',min, ',',

   max, ']')

  else break;

 until false;

 readNumber:=a;

end;

 

procedure readMatrix1 (var n:integer;

 var a:matrix);

var i,j:integer; s,s1:string;

begin

 n:=readNumber ('Введите число строк ',

  'и столбцов матрицы: ',2,cities);

 for i:=1 to n do

 for j:=i+1 to n do begin

  s:='A['; str(i,s1); s:=s+s1+',';

  str(j,s1); s:=s+s1+']=';

  a[i,j]:=readNumber (s,0,maxInt);

 end;

end;

 

procedure writeMatrix1 (s:string;

 n:integer; var a:matrix);

var i,j:integer;

begin

 writeln (s);

 for i:=1 to n do begin

  for j:=1 to n do write (a[i,j]:7);

  writeln;

 end;

end;

 

var a:matrix;

    n,gorod1,gorod2:integer;

 

begin

 readMatrix1 (n,a);

 max1 (n,a,gorod1,gorod2);

 writeMatrix1 ('A=',n,a);

 writeln ('Самый большой пассажиропоток ',

  'между городами ',gorod1,' и ',gorod2);

 readln;

end.

 

18.4. Открытые массивы

 

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

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

Имеющиеся в программе векторы описываются в разделе var обычным способом, без определения типа type. В списке формальных параметров подпрограммы параметр-вектор указывается без диапазона размерности:

function sum(var x : array of real) : real;

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

var a:array [1..5] of real;

 s:real;

...

s:=sum(a);

Открытым остается вопрос -- как отслеживать из подпрограммы размерность переданного массива? Для этого в Паскале существуют стандартные функции Low и High. Их единственным параметром передается идентификатор массива, Low возвращает самое низкое допустимое значение индекса, а High -- самое высокое. Если A -- одномерный массив, величины Low(A) и High(A) можно непосредственно применять как границы цикла for:

function sum(var x : array of real) : real;

var i:word; s:real;

begin

 s:=0;

 for i:=Low(x) To High(x) Do s:=s+x[I];

 sum:=s;

end;

Чтобы завершить пример, вызовем написанную функцию sum:

const a:array [1..5] of real=(1,2,3,4,5.5);

begin

 writeln (sum(a):6:1);

end.

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

Найти количество элементов вектора x[7], попадающих в интервал [0, 3] и  количество элементов вектора y[5], попадающих в интервал [-1, 1].

Для ввода элементов массива с клавиатуры напишем процедуру Input, которой все-таки придется передавать размерность массива-параметра (ведь вводятся два вектора различной размерности). Поэтому в Input использован тот факт, что нумерация элементов открытого массива по умолчанию выполняется в нуля. Функции kol, вычисляющей количество элементов открытого массива, попадающих в интервал [x1,x2], достаточно стандартного указания Low и High:

var x:array [1..7] of real;

    y:array [1..5] of real;

    k1,k2,i:integer;

 

procedure Input (n:integer;

 var a:array of real);

var i:integer;

begin

 writeln ('Enter ',n,' items of array:');

 for i:=0 to n-1 do read (a[i]);

end;

 

function Kol (var a:array of real;

 x1,x2:real):integer;

var i,k:integer;

begin

 k:=0;

 for i:=Low(a) to High(a) do

  if (a[i]>=x1) and (a[i]<=x2) then k:=k+1;

 Kol:=k;

end;

 

begin

 Input (7,x);

 Input (5,y);

 k1:=Kol(x,0,3);

 k2:=Kol(y,-1,1);

 writeln ('k1=',k1,' k2=',k2);

end.

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

procedure Input (var a:array of real);
var i:integer;
begin
 writeln ('Enter ',High(a)-Low(a)+1,
  ' items of array:');
 for i:=Low(a) to High(a) do read (a[i]);
end;
{ . . . }
Input (x);
Input (y);

Рейтинг@Mail.ru
вверх гостевая; E-mail
Hosted by uCoz