Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

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

Передачу по значению мы рассмотрели в прошлом уроке (это когда под формальные переменные выделяется память и в эту память записываются значения фактических параметров. При этом при изменении значений таких формальных переменных значения фактических переменных не изменяется (то есть, если внутри подпрограммы мы изменили значение переменной, передаваемой по значению, то оригинальное значение данной переменной не изменится и внутри основной программы оно будет тем же, что и до вызова подпрограммы). Этот способ передачи данных поэтому немного неудобен, ведь намного больше случаев в практике встречается, когда из подпрограммы нужно вернуть одно или несколько значений. Когда нужно вернуть только одно значение, то можно воспользоваться функцией, а вот когда нужно вернуть два или более что делать? На основе тех знаний, которые у нас уже есть, мы не обойдемся, ну или же можно попробовать помудрить с оператором goto. Но для этого придумали передачу параметров по ссылке. Благодаря этому мы можем внутри подпрограммы изменить значение какой-либо переменной и это изменение будет доступно вне подпрограммы. Для этого перед входно-выходными параметрами нужно поставить ключевое слово «var».

procedure Procedure_Name(var Result_input:Type);

begin

  Action;

end;

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

procedure SP(a,b:real; var S,P:real);

begin

  s:=a*b;

  p:=2*(a+b);

end;

var s,p:real;

begin

  while true do

  begin

    sp(readreal(),readreal(),s,p);

    writeln('S = ',s,' P = ',p);

  end;

end.

В процедуре «SP» первых два параметра передаются по значению, а последние два - по ссылке. Почему этот способ называется передачей по ссылке Вы поймете чуть позже, когда будет изучать тему указатели и динамическая память. А теперь разберем этот код.

Опять же вначале мы видим обьявление процедуры, которая принимает 4 параметра, и высчитывает площадь и периметр прямоугольника по стандартным формулам. После обьяления процедуры идет описание двух переменных S и P. Почуму я их так назвал всем должно быть понятно. В коде основной программы мы видим бесконечный цикл, который вызывает процедуру «sp» с двумя параметрами, которые вводятся с клавиатуры, а другие два это переменные s u p, которые мы передаем по ссылке. (!)(Внимание! Если подпрограмма принимает данный параметр по ссылке, то этот параметр при вызове подпрограммы должен быть переменной ровно того типа, который указан при описании подпрограммы. (при передаче по значению в качестве передаваемого параметра можно использовать выражения, а при передаче по ссылке должны быть только переменные)) В этих переменных программа будет хранить найденные значения площади и периметра прямоугольника со сторонами a u b. После выполнения подпрограммы, компьютер выводит на экран найденные значения площади и периметра.

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

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

Однако у этого способа есть один существенный минус: вы не знаете, что будет происходить с вашим значением внутри подпрограммы, если не Вы писали эту подпрограмму. А вдруг Вам заявили, что данная подпрограмма принимает параметры по ссылке только для большего быстродействия, а на самом деле она портит Вам ваши данные? Вы скажите, что можно просмотреть реализацию и сразу сказать, что она делает. Но нет. Иногда разработчики пользуются стилем запутывания (это когда пишут программу так, что ее почти невозможно понять) или Вам могут дать откомпилированный код (код программы в машинном коде, которые нельзя превратить в код понятный программисту). В таких случаях невозможно узнать, что тварится внутри подпрограммы и что происходит с вашей переменной. Скорее всего Вы скажете, что тогда нужно передавать параметры по значению. Но это бывает крайне не эффективно. Для того, чтобы Вы лучше поняли почему это не эффективно, я приведу пример: допустим у Вас в программе есть массив данных с типом real, в котором хринится 536870912 записей, что займет в памяти около 4 Гб. И Вам нужно вычислить среднее арифметическое среди всех этих данных. Если передавать этот массив по значению, то вашему компьютеру потребуется больше 8Гб оперативной памяти. Не знаю, как у Вас, но у меня только 6 Гб. Притаком способе из-за нехватки памяти либо компьютер напроч зависнет, либо ОС завершит Вашу программу. Но в обеих случаях Вы потеряете все данные. Однако все будет нормально, если у Вас на компьютере стоит более 8 Гб оперативной памяти. А если передавать по ссылке, то подпрограмма может попортить все ваши данные.

///Программа принимает два параметра по ссылке (для большего быстробействия) и возвращает их сумму

function Sum(var a,b:real):real;

begin

  result:=a+b;

  a:=3.14;

  b:=666;

end;

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

Такой способ передачи данных гарантирует нам большое быстродействие и неизменность наших данных. Для передачи данных таким способ нужно перед формальной переменной поставить ключевое слово «const».

procedure Procedure_Name(const Result_input:Type);

begin

  Action;

end;

Для примера я просто приведу код процедуры нахождения площади прямоугольника.

procedure SP(const a,b:real; var S,P:real);

begin

  s:=a*b;

  p:=2*(a+b);

end;

В этом коде первые две переменные передаются по ссылке, но без возможности их редактирования. Если внутри такой подпрограммы попробовать отредактировать такие данные, но будет ошибка «Program1.pas(5) : Невозможно присвоить константному объекту».

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

В Паскале есть еще один способ передачи данных. Ну вообще это не совсем способ. Это просто обращение к глобальным данным.

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

var a,b:real;

procedure SP(var S,P:real);

begin

  s:=a*b;

  p:=2*(a+b);

  //если я сделаю процедуру без параметров и попытаюсь обратиться к переменным s или p, то будет ошибка, т.к. их описание стоит после описание этой процедуры

end;

var s,p:real;

begin

  while true do

  begin

    a:=readreal();

    b:=readreal();

    sp(s,p);

    writeln('S = ',s,' P = ',p);

  end;

end.

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

1)очень сильно падает быстродействие, потому что компьютеру придутся при каждом обращении к этой переменной обыскивать весь стек памяти, чтобы найти нужную переменную, и в некоторых случаях он может найти не ту переменную, которая нужна.

2)при таком обращении к переменным есть возможность отредактировать данные без вашего ведома.

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

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

Теперь рассмотрим еще одну особенность подпрорамм - параметры по-умолчанию.

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

Для того, чтобы так сделать, нужно при описании параметра, который мы хотим сделать параметром по-умолчанию, указать его тип и указать его знаение. В итоге должно получиться «Name:type:=Value». (!)(Внимание! В программе можно сделать несколько параметров по-умолчанию, но все они должны стоять вконце описания параметров. Если после таких параметров сделать параметр не по-умолчанию, то компилятор выдаст ошибку).

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

function Calc(const a,b:real; c:char:='+'):real;

begin

  case c of

    '+':result:=a+b;

    '-':result:=a-b;

    '/':result:=a/b;

    '*':result:=a*b;

    else raise new System.Exception('Некорректные данные');

  end;

end;

var a,b:real;

var c:char;

begin

  while true do

  begin

    write('A = ');

    a:=readlnreal();

    write('B = ');

    b:=readlnreal();

    write('Command = ');

    c:=readlnchar();

    writeln('Result = ',calc(a,b,c));

  end;

end.

В самом начале программы мы видим описание функции, которая принимает три переменные, самый последний из которых с параметром по-умолчанию. (!) (Внимание! пораметр по-умолчанию должен передаваться только по значению) При вызове такой функции можно писать не все параметры «calc(a,b)», потому что самый последний из них все равно примет значение, которое указано при описании. Данная функция по значению параметра «c» определяет, какое действие нужно сделать с параметрами «a» и «b» с помощью оператора case. А сама программа бесконечное кол-во раз считывает с клавиатуры значения переменных «a» u «b» и комманду «c» и передает их функции, котороая это все высчитывает. Вы уже это все знаете, поэтому для Вас этот код должен быть очевидным.

Иногда нужно преждевременно остановить выполенние программы/подпрограммы. Как остановить выполнение цикла мы знаем, а как остановить выполнение самой программы? Для этого сделан оператор «exit», который завершает выполнение текущей программы (или подпрограммы в зависимости от того, где он находится). Для примера рассмотрим подпрограмму, которая проверяет есть ли среди введенных чисел 0.

function HasZero(n:integer):boolean;

begin

  assert(n>0);

  result:=false;

  for var i:integer:=1 to n do

  begin

    var x:integer:=readinteger();

    if (x=0) then

    begin

      result:=true;

      exit;

    end;

  end;

end;

begin

  writeln(HasZero(5));

end.

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

Подпрограмма для начала проверяет параметр n на положительность (если кол-во чисел, которое нужно считать отрицательно, то должна быть ошибка). После этого подпрограмма заранее записывает в возвращаемое значение «false» (0 не найден). Далее идет цикл, который идет по кол-ву, при этом каждый раз программа выделяет память под переменную «х» типа интеджер и считыает ее с клавиатуры. Далее, если «х» равен 0, то программа возвращает значение «true» (ноль найден) и спомощью оператора exit завершает выполнение подпрограммы. Если значение не равно 0, то программа продолжает выполнение цикла. И так далее до тех пор, пока не будет найден 0 или не закончится цикл.

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

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

Если нам это понадобилось, то реализовать то таким способом не получиться

procedure p;

begin

  q();

end;

procedure q;

begin

  p();

end;

begin

  p();

end.

Будет ошибка компиляции: «Program1.pas(3) : Неизвестное имя 'q'».

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

procedure q; forward;

procedure p;

begin

  q();

end;

procedure q;

begin

  p();

end;

begin

  p();

end.

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

Иногда нужно написать одну и ту же подпрограмму так, чтобы она работала для разных типов данных. Например в Паскале есть процедура «swap», которая меняет принимает две переменные по ссылке и меняет их значения местами. Вполне понятно, что эта подпрограмма должна работать для разных типов данных. Написать для одного типа данных мы знаем. А как для другого? Да точно также как и для одного, только указать другие типы данных.

procedure swap(var a,b:integer);

begin

  var c:=a;

  a:=b;

  b:=c;

end;

procedure swap(var a,b:string);

begin

  var c:=a;

  a:=b;

  b:=c;

end;

procedure swap(var a,b:real);

begin

  var c:=a;

  a:=b;

  b:=c;

end;

begin

  var a:=3;

  var c:=5;

  swap(a,c);

end.

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

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

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

Описываются такие типы просто:

процедурный тип

var Action:procedure(Parametr);

функциональный тип

var Myfunction:function(Parametr):Type;

где Action/Myfunction - это имена переменных подпрограмм

parametr - это список параметров, которые должны принимать подпрограммы.

Type - тип возвращаемого значения функции.

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

var s:int64;

procedure ActionPredicate(n:integer; predicate:function(const x:integer):boolean; action:procedure(var x:integer));

begin

  for var i:integer:=1 to n do

  begin

  var x:integer:=readinteger();

  if (predicate(x)) then

  action(x);

  end;

end;

procedure Sum(var x:integer);

begin

  s+=x;

end;

function IsNotOdd(const x:integer):boolean;

begin

  result:=not odd(x);

end;

begin

  ActionPredicate(5,IsNotOdd,Sum);

  writeln('Sum = ',s);

end.

Давайте разберем этот код. В самом начале я обьявил переменную «s», которая будет хранить данные о сумме. (в подпрограмме будет использована глабальная переменная. Хоть лучше так не делать, но я сделаю для большей гибкости). Далее идет процедура, которая принимает кол-во переменных, которое нужно считать с клавиатуры, функцию, которая будет принимать одно константное значение типа integer и возвращать логическое значение, и процедуру, которая принимает один параметр по ссылке. Эта подпрограмма идет циклом по кол-ву значений. Создает переменную «х», которую считывает с клавиатуры. Далее подпрограмма проверяет на истинность для этого значения переданной подпрограмме функции. И, если функция на этом значении дает истинность, то вызывается переданная ей процедура. Далее идет процедура, которая производит суммирование всего того, что ей было передано. Далее идет процедура, которая проверяет на нечетность. А в коде основной программы идет вызов самой первой процедуры, передавая ей значение 5 (нужно считать 5 чисел с клавиатуры), функцию определения четности и процедуру суммирования. В итоге мы получили вызов процедуры, которая считывает с клавиатуры определенное кол-во значений и подсчитывает сумму четных значений.

Если нам нужно вывести на экран после каждого четного значения строку «четное число», то можно написать еще одну процедуру «procedure tmp(var x:integer); begin writeln('Число четное'); end;» и следующий вызов «ActionPredicate(7,odd,tmp);». Так мы получили очень большую гибкость. И как ей пользоваться - решать Вам.

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

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

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

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

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